Compare commits

...

30 Commits

Author SHA1 Message Date
tom5079
114158cf73 fix backup file selection, support hasha link 2024-01-15 19:01:30 -08:00
tom5079
6d108dd7ff fix backup, notification for android 33+ 2024-01-14 14:30:17 -08:00
tom5079
f36b7f1dbe Update README.md 2022-07-20 09:05:47 -07:00
tom5079
0a22ebd8e9 Merge remote-tracking branch 'origin/master' 2022-07-20 09:04:35 -07:00
tom5079
3682eeaf94 Fix image not retrying 2022-07-20 09:04:23 -07:00
tom5079
7df2ae4ba7 Update README.md 2022-07-19 20:31:42 -07:00
tom5079
c9519ec681 Fix image not retrying 2022-07-19 20:29:39 -07:00
tom5079
b146ed684d Fix app crashing when recovering metadata is corrupt 2022-05-31 08:06:48 +09:00
tom5079
d2787c36d7 Update README.md 2022-04-24 20:39:17 +09:00
tom5079
3ff663114a 5.3.7 2022-04-24 20:39:01 +09:00
tom5079
573e62f310 5.3.7 2022-04-24 20:36:56 +09:00
tom5079
f9af670b82 Update README.md 2022-04-24 20:21:41 +09:00
tom5079
bf461475c6 Merge remote-tracking branch 'origin/master' 2022-04-24 20:20:55 +09:00
tom5079
bdea6e0cc1 Use System.currentTimeMillis() instead of Instant 2022-04-24 20:20:45 +09:00
tom5079
57f0ec4e5d Update README.md 2022-04-24 18:44:10 +09:00
tom5079
d663092363 v5.3.5 2022-04-24 18:27:56 +09:00
tom5079
edf6188e36 Merge pull request #126 from tom5079/Pupil-116
Pupil 116 Favorite tag backup
2022-04-24 18:08:13 +09:00
tom5079
f3f3395e68 Merge pull request #128 from tom5079/Pupil-127
Pupil-127 Use gg.js directly
2022-04-24 18:08:04 +09:00
tom5079
ac9dc347e3 Pupil-127 Use gg.js directly 2022-04-22 16:39:13 +09:00
tom5079
8721d85946 Show ProgressDrawable when backup 2022-04-21 17:41:06 +09:00
tom5079
a0bd1a8738 Pupil-116 Add favorite tags backup 2022-04-21 17:26:58 +09:00
tom5079
35fdf3e3b0 Update PreferenceFragments to comply with updated library 2022-04-21 16:49:13 +09:00
tom5079
aced8293f1 Dependency Update 2022-04-21 16:45:13 +09:00
tom5079
3f516faad8 AGP update 2022-04-21 16:42:34 +09:00
tom5079
824f7b9602 Merge remote-tracking branch 'origin/master' 2022-04-16 06:30:31 +09:00
tom5079
95aeeaa16f updated .gitignore 2022-04-16 06:30:10 +09:00
tom5079
63f08f0230 Fixed Downloaded folder gets deleted when opened with no network 2022-03-25 16:48:38 +09:00
tom5079
3b241fe857 Delete watchdiff.yml 2022-02-02 06:38:50 +09:00
tom5079
75bc104f43 Update watchdiff.yml 2022-02-02 05:57:55 +09:00
tom5079
30afd56324 Update README.md 2022-02-01 19:11:55 +09:00
41 changed files with 438 additions and 684 deletions

View File

@@ -1,23 +0,0 @@
# This is a basic workflow that is manually triggered
name: Watch hitomi.la file changes
on:
schedule:
- cron: "*/10 * * * *"
jobs:
watchdiff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: watchdiff
- name: Download files
run: ./fetch.sh
- name: Commit and Push
id: push
run: |
git config --global user.name 'Watchdiff bot'
git config --global user.email 'tom5079@naver.com'
{ git add . && git commit -m "File update" && git push; } | tail -1 | grep -q "nothing to commit"

47
.gitignore vendored
View File

@@ -1,20 +1,33 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml *.iml
.gradle .idea/
/local.properties misc.xml
/.idea/caches deploymentTargetDropDown.xml
/.idea/libraries render.experimental.xml
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
#Github pages # Keystore files
/gh-pages *.jks
*.keystore
#Private files # Google Services (e.g. APIs or Firebase)
**/google-services.json google-services.json
**/credentials.json
# Android Profiling
*.hprof

View File

@@ -1,139 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Pupil, Hitomi.la viewer for Android&#10; Copyright (C) &amp;#36;today.year tom5079&#10;&#10; This program is free software: you can redistribute it and/or modify&#10; it under the terms of the GNU General Public License as published by&#10; the Free Software Foundation, either version 3 of the License, or&#10; (at your option) any later version.&#10;&#10; This program is distributed in the hope that it will be useful,&#10; but WITHOUT ANY WARRANTY; without even the implied warranty of&#10; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10; GNU General Public License for more details.&#10;&#10; You should have received a copy of the GNU General Public License&#10; along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;." />
<option name="myName" value="GPL" />
</copyright>
</component>

View File

@@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Project Files" copyright="GPL" />
</module2copyright>
</settings>
</component>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-02-01T08:00:57.223690Z" />
</component>
</project>

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="tom50">
<words>
<w>hitomi</w>
</words>
</dictionary>
</component>

4
.idea/encodings.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

22
.idea/gradle.xml generated
View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="/usr/share/java/gradle" />
<option name="gradleJvm" value="11" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -1,100 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="http://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://s3.amazonaws.com/fabric-artifacts-private/internal-snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://maven.fabric.io/public" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://dl.bintray.com/tom5079/maven" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="http://dl.bintray.com/piasy/maven" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://guardian.github.io/maven/repo-releases/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:$USER_HOME$/.m2/repository/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://maven.mozilla.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" />
</remote-repository>
</component>
</project>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinCodeInsightWorkspaceSettings">
<option name="addUnambiguousImportsOnTheFly" value="true" />
<option name="optimizeImportsOnTheFly" value="true" />
</component>
</project>

6
.idea/kotlinc.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
</project>

20
.idea/misc.xml generated
View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../layout/custom_preview.xml" value="0.2564814814814815" />
<entry key="app/src/main/res/layout/reader_activity.xml" value="0.14351851851851852" />
<entry key="app/src/main/res/xml/lock_preferences.xml" value="0.5119791666666667" />
<entry key="app/src/main/res/xml/manage_storage_preferences.xml" value="0.2604166666666667" />
<entry key="app/src/main/res/xml/root_preferences.xml" value="0.5119791666666667" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="Pupil" pattern="file[app]:*/" />
</component>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -2,7 +2,7 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total) ![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.3/Pupil-v5.3.3.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.3/Pupil-v5.3.3.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.8-hotfix1/Pupil-v5.3.8-hotfix1.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.8-hotfix1/Pupil-v5.3.8-hotfix1.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features # Features

View File

@@ -32,13 +32,13 @@ configurations {
} }
android { android {
compileSdkVersion 31
defaultConfig { defaultConfig {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 31 compileSdk 34
targetSdkVersion 34
versionCode 69 versionCode 69
versionName "5.3.4" versionName "5.3.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@@ -79,13 +79,14 @@ android {
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
implementation "androidx.appcompat:appcompat:1.4.1" implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.4.0" implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.0" implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.3" implementation "androidx.constraintlayout:constraintlayout:2.1.3"
implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.gridlayout:gridlayout:1.0.0"
@@ -94,15 +95,15 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.5.0" implementation "com.google.android.material:material:1.11.0"
implementation platform('com.google.firebase:firebase-bom:29.0.3') implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation "com.google.firebase:firebase-analytics-ktx" implementation "com.google.firebase:firebase-analytics-ktx"
implementation "com.google.firebase:firebase-crashlytics-ktx" implementation "com.google.firebase:firebase-crashlytics-ktx"
implementation "com.google.firebase:firebase-perf-ktx" implementation "com.google.firebase:firebase-perf-ktx"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0" implementation "com.google.android.gms:play-services-oss-licenses:17.0.1"
implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.0" implementation "com.google.android.gms:play-services-mlkit-face-detection:17.1.0"
implementation "com.github.clans:fab:1.6.4" implementation "com.github.clans:fab:1.6.4"
@@ -128,15 +129,11 @@ dependencies {
implementation "org.jsoup:jsoup:1.14.3" implementation "org.jsoup:jsoup:1.14.3"
implementation ("app.cash.zipline:zipline:1.0.0-SNAPSHOT") {
exclude group: "com.squareup.okio", module: "okio"
}
implementation "xyz.quaver:documentfilex:0.7.2" implementation "xyz.quaver:documentfilex:0.7.2"
implementation "xyz.quaver:floatingsearchview:1.1.7" implementation "xyz.quaver:floatingsearchview:1.1.7"
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0" androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0" androidTestImplementation "androidx.test:runner:1.4.0"

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.3.4", "versionName": "5.3.10",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -11,6 +11,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@@ -45,7 +47,8 @@
</provider> </provider>
<service android:name=".services.DownloadService" <service android:name=".services.DownloadService"
android:exported="false"/> android:exported="false"
android:foregroundServiceType="specialUse" />
<receiver <receiver
android:name=".receiver.UpdateBroadcastReceiver" android:name=".receiver.UpdateBroadcastReceiver"
@@ -61,165 +64,107 @@
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:parentActivityName=".ui.MainActivity" android:parentActivityName=".ui.MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/galleries" <data android:host="*.hasha.in"/>
android:scheme="http" /> <data android:pathPrefix="/reader"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/manga" <data android:host="hitomi.la"/>
android:scheme="http" /> <data android:pathPrefix="/galleries"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/doujinshi" <data android:host="hitomi.la" />
android:scheme="http" /> <data android:pathPrefix="/manga" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/cg" <data android:host="hitomi.la" />
android:scheme="http" /> <data android:pathPrefix="/doujinshi" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/reader" <data android:host="hitomi.la" />
android:scheme="http" /> <data android:pathPrefix="/cg" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/galleries" <data android:host="hitomi.la" />
android:scheme="https" /> <data android:pathPrefix="/imageset" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:scheme="https" />
android:pathPrefix="/manga" <data android:host="hitomi.la" />
android:scheme="https" /> <data android:pathPrefix="/reader" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="http" />
android:host="hitomi.la" <data android:host="e-hentai.org" />
android:pathPrefix="/doujinshi" <data android:pathPrefix="/g" />
android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="https" />
android:host="hitomi.la" <data android:host="e-hentai.org" />
android:pathPrefix="/cg" <data android:pathPrefix="/g" />
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hitomi.la"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hiyobi.me"
android:scheme="http"
android:pathPrefix="/reader" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="hiyobi.me"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="e-hentai.org"
android:pathPrefix="/g"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="e-hentai.org"
android:pathPrefix="/g"
android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.SettingsActivity" android:name=".ui.SettingsActivity"
android:label="@string/settings_title"> android:label="@string/settings_title">
<tools:validation testUrl="http://ix.io/eer" />
</activity> </activity>
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
@@ -232,17 +177,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="http"
android:host="ix.io"
android:pathPattern="/..*" />
</intent-filter>
</activity> </activity>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" /> <activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
</application> </application>

View File

@@ -30,7 +30,6 @@ import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import app.cash.zipline.QuickJs
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.fresco.FrescoImageLoader import com.github.piasy.biv.loader.fresco.FrescoImageLoader
@@ -77,46 +76,12 @@ val client: OkHttpClient
clientHolder = it clientHolder = it
} }
private var version = ""
var runtimeReady = false
private set
lateinit var runtime: QuickJs
class Pupil : Application() { class Pupil : Application() {
companion object { companion object {
lateinit var instance: Pupil lateinit var instance: Pupil
private set private set
} }
init {
CoroutineScope(Dispatchers.IO).launch {
withContext(Dispatchers.Main) {
runtime = QuickJs.create()
}
while (true) {
kotlin.runCatching {
val newVersion = URL("https://tom5079.github.io/PupilSources/hitomi.html.ver").readText()
if (version != newVersion) {
runtimeReady = false
evaluationContext.cancelChildren()
kotlin.runCatching {
URL("https://tom5079.github.io/PupilSources/assets/js/gg.js").readText()
}.getOrNull()?.also { gg ->
withContext(Dispatchers.Main) {
runtime.evaluate(gg)
version = newVersion
runtimeReady = true
}
}
}
}
delay(10000)
}
}
}
override fun onCreate() { override fun onCreate() {
instance = this instance = this
@@ -135,7 +100,7 @@ class Pupil : Application() {
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder() clientBuilder = OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS) // .connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS)
.proxyInfo(proxyInfo) .proxyInfo(proxyInfo)
.addInterceptor { chain -> .addInterceptor { chain ->

View File

@@ -17,16 +17,23 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock.System.now
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.runtime
import xyz.quaver.pupil.runtimeReady
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime
const val protocol = "https:" const val protocol = "https:"
@@ -131,19 +138,73 @@ const val nozomiextension = ".nozomi"
val evaluationContext = Dispatchers.Main + Job() val evaluationContext = Dispatchers.Main + Job()
object gg { object gg {
private var lastRetrieval: Long? = null
suspend fun m(g: Int): Int = withContext(evaluationContext) { private val mutex = Mutex()
while (!runtimeReady) delay(1000)
runtime.evaluate("gg.m($g)").toString().toInt() private var mDefault = 0
} private val mMap = mutableMapOf<Int, Int>()
suspend fun b(): String = withContext(evaluationContext) {
while (!runtimeReady) delay(1000) private var b = ""
runtime.evaluate("gg.b").toString()
@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class)
private suspend fun refresh() = withContext(Dispatchers.IO) {
mutex.withLock {
if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) {
val ggjs: String = suspendCancellableCoroutine { continuation ->
val call = client.newCall(Request.Builder().url("https://ltn.hitomi.la/gg.js").build())
call.enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
} }
suspend fun s(h: String): String = withContext(evaluationContext) { override fun onResponse(call: Call, response: Response) {
while (!runtimeReady) delay(1000) if (!call.isCanceled) {
runtime.evaluate("gg.s('$h')").toString() response.body()?.use {
continuation.resume(it.string()) {
call.cancel()
}
}
}
}
})
continuation.invokeOnCancellation {
call.cancel()
}
}
mDefault = Regex("var o = (\\d)").find(ggjs)!!.groupValues[1].toInt()
val o = Regex("o = (\\d); break;").find(ggjs)!!.groupValues[1].toInt()
mMap.clear()
Regex("case (\\d+):").findAll(ggjs).forEach {
val case = it.groupValues[1].toInt()
mMap[case] = o
}
b = Regex("b: '(.+)'").find(ggjs)!!.groupValues[1]
lastRetrieval = System.currentTimeMillis()
}
}
}
suspend fun m(g: Int): Int {
refresh()
return mMap[g] ?: mDefault
}
suspend fun b(): String {
refresh()
return b
}
fun s(h: String): String {
val m = Regex("(..)(.)$").find(h)
return m!!.groupValues.let { it[2]+it[1] }.toInt(16).toString(10)
} }
} }

View File

@@ -23,6 +23,7 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
@@ -100,13 +101,15 @@ class DownloadService : Service() {
notify(galleryID) notify(galleryID)
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi", "MissingPermission")
private fun notify(galleryID: Int) { private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0 val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
val notification = notification[galleryID] ?: return val notification = notification[galleryID] ?: return
if (!checkNotificationEnabled(this)) return
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
notification notification
.setContentText(getString(R.string.reader_notification_complete)) .setContentText(getString(R.string.reader_notification_complete))
@@ -168,19 +171,26 @@ class DownloadService : Service() {
private val interceptor: PupilInterceptor = { chain -> private val interceptor: PupilInterceptor = { chain ->
val request = chain.request() val request = chain.request()
var response = chain.proceed(request) var response = kotlin.runCatching {
var limit = 5 chain.proceed(request)
}.getOrNull()
var limit = 10
while (!response.isSuccessful) { while (response?.isSuccessful != true) {
if (response.code() == 503) { if (response?.code() == 503) {
Thread.sleep(200) Thread.sleep(200)
} else if (--limit > 0) } else if (--limit < 0)
break break
response = chain.proceed(request) response = kotlin.runCatching {
chain.proceed(request)
}.getOrNull()
} }
response.newBuilder() if (response == null)
response = chain.proceed(request)
response!!.newBuilder()
.body(response.body()?.let { .body(response.body()?.let {
ProgressResponseBody(request.tag(), it, progressListener) ProgressResponseBody(request.tag(), it, progressListener)
}).build() }).build()
@@ -207,6 +217,7 @@ class DownloadService : Service() {
private val callback = object: Callback { private val callback = object: Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.d("PUPILD", "ONFAILURE ${call.request().tag()}, ${e}")
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
if (e.message?.contains("cancel", true) == false) { if (e.message?.contains("cancel", true) == false) {
@@ -215,6 +226,7 @@ class DownloadService : Service() {
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
val (galleryID, index, startId) = call.request().tag() as Tag val (galleryID, index, startId) = call.request().tag() as Tag
val ext = call.request().url().encodedPath().split('.').last() val ext = call.request().url().encodedPath().split('.').last()
@@ -394,7 +406,11 @@ class DownloadService : Service() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
startForeground(R.id.downloader_notification_id, serviceNotification.build()) startForeground(R.id.downloader_notification_id, serviceNotification.build())
} else {
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
}
when (intent?.getStringExtra(KEY_COMMAND)) { when (intent?.getStringExtra(KEY_COMMAND)) {
COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
@@ -415,7 +431,11 @@ class DownloadService : Service() {
override fun onBind(p0: Intent?) = binder override fun onBind(p0: Intent?) = binder
override fun onCreate() { override fun onCreate() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
startForeground(R.id.downloader_notification_id, serviceNotification.build()) startForeground(R.id.downloader_notification_id, serviceNotification.build())
} else {
startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
}
interceptors[Tag::class] = interceptor interceptors[Tag::class] = interceptor
} }

View File

@@ -18,8 +18,10 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -31,7 +33,9 @@ import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -58,10 +62,12 @@ import xyz.quaver.pupil.ui.view.MainView
import xyz.quaver.pupil.ui.view.ProgressCard import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.requestNotificationPermission
import xyz.quaver.pupil.util.checkUpdate import xyz.quaver.pupil.util.checkUpdate
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
@@ -107,6 +113,12 @@ class MainActivity :
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater) binding = MainActivityBinding.inflate(layoutInflater)
@@ -118,12 +130,14 @@ class MainActivity :
onFailure = { onFailure = {
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = { }, onSuccess = {
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
} }
) )
} }
} }
requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {}
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
@@ -392,6 +406,10 @@ class MainActivity :
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position] val galleryID = galleries[position]
requestNotificationPermission(
this@MainActivity,
requestNotificationPermssionLauncher
) {
if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID) DownloadService.cancel(this@MainActivity, galleryID)
} }
@@ -399,6 +417,7 @@ class MainActivity :
DownloadManager.getInstance(context).addDownloadFolder(galleryID) DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID) DownloadService.download(this@MainActivity, galleryID)
} }
}
closeAllItems() closeAllItems()
} }

View File

@@ -57,9 +57,12 @@ import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.camera import xyz.quaver.pupil.util.camera
import xyz.quaver.pupil.util.checkNotificationEnabled
import xyz.quaver.pupil.util.closeCamera import xyz.quaver.pupil.util.closeCamera
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.requestNotificationPermission
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
import xyz.quaver.pupil.util.startCamera import xyz.quaver.pupil.util.startCamera
class ReaderActivity : BaseActivity() { class ReaderActivity : BaseActivity() {
@@ -117,6 +120,12 @@ class ReaderActivity : BaseActivity() {
private lateinit var binding: ReaderActivityBinding private lateinit var binding: ReaderActivityBinding
private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ReaderActivityBinding.inflate(layoutInflater) binding = ReaderActivityBinding.inflate(layoutInflater)
@@ -148,10 +157,11 @@ class ReaderActivity : BaseActivity() {
val uri = intent.data val uri = intent.data
val lastPathSegment = uri?.lastPathSegment val lastPathSegment = uri?.lastPathSegment
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
galleryID = when (uri.host) { galleryID = if (uri.host?.endsWith("hasha.in") == true) {
lastPathSegment?.toInt() ?: 0
} else when (uri.host) {
"hitomi.la" -> "hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0 Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
"hiyobi.me" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt() "e-hentai.org" -> uri.pathSegments[1].toInt()
else -> 0 else -> 0
} }
@@ -360,6 +370,10 @@ class ReaderActivity : BaseActivity() {
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
setOnClickListener { setOnClickListener {
requestNotificationPermission(
this@ReaderActivity,
requestNotificationPermssionLauncher
) {
val downloadManager = DownloadManager.getInstance(this@ReaderActivity) val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
if (downloadManager.isDownloading(galleryID)) { if (downloadManager.isDownloading(galleryID)) {
@@ -372,6 +386,7 @@ class ReaderActivity : BaseActivity() {
} }
} }
} }
}
with(binding.retryFab) { with(binding.retryFab) {
setImageResource(R.drawable.refresh) setImageResource(R.drawable.refresh)

View File

@@ -18,25 +18,72 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Resources
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import okhttp3.* import okhttp3.*
import xyz.quaver.io.FileX
import xyz.quaver.io.util.readText
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.get
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.math.roundToInt
class ManageFavoritesFragment : PreferenceFragmentCompat() { class ManageFavoritesFragment : PreferenceFragmentCompat() {
private val requestBackupFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
return@registerForActivityResult
}
val uri = result.data?.data ?: return@registerForActivityResult
val context = context ?: return@registerForActivityResult
val view = view ?: return@registerForActivityResult
val backupData = runCatching {
FileX(context, uri).readText()?.let { Json.parseToJsonElement(it) }
}.getOrNull() ?: run{
Snackbar.make(view, context.getString(R.string.error), Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
val newFavorites = backupData["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
val newFavoriteTags = backupData["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
favorites.addAll(newFavorites)
favoriteTags.addAll(newFavoriteTags)
Snackbar.make(view, context.getString(R.string.settings_restore_success, newFavorites.size + newFavoriteTags.size), Snackbar.LENGTH_LONG).show()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey) setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
@@ -47,57 +94,44 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
val context = context ?: return val context = context ?: return
findPreference<Preference>("backup")?.setOnPreferenceClickListener { findPreference<Preference>("backup")?.setOnPreferenceClickListener {
val request = Request.Builder() val favorites = runCatching {
.url(context.getString(R.string.backup_url)) Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText())
.post( }.getOrNull()
FormBody.Builder() val favoriteTags = kotlin.runCatching {
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText()) Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText())
.build() }.getOrNull()
).build()
client.newCall(request).enqueue(object: Callback { val favoriteJson = buildJsonObject {
override fun onFailure(call: Call, e: IOException) { favorites?.let {
val view = view ?: return put("favorites", it)
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() }
favoriteTags?.let {
put("favorite_tags", it)
}
} }
override fun onResponse(call: Call, response: Response) { val backupFile = File(context.filesDir, "pupil-backup.json").also {
if (response.code() != 200) { it.writeText(favoriteJson.toString())
response.close()
return
} }
Intent(Intent.ACTION_SEND).apply { Intent(Intent.ACTION_SEND).apply {
type = "text/plain" val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", backupFile)
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", "")) setDataAndType(uri, "application/json")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
}.let { }.let {
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share))) context.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
} }
}
})
true true
} }
findPreference<Preference>("restore")?.setOnPreferenceClickListener { findPreference<Preference>("restore")?.setOnPreferenceClickListener {
val editText = EditText(context).apply { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
} }
AlertDialog.Builder(context) requestBackupFileLauncher.launch(intent)
.setTitle(R.string.settings_restore_title)
.setView(editText)
.setPositiveButton(android.R.string.ok) { _, _ ->
restore(editText.text.toString(),
onFailure = onFailure@{
val view = view ?: return@onFailure
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = onSuccess@{
val view = view ?: return@onSuccess
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
})
}.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do Nothing
}.show()
true true
} }

View File

@@ -61,12 +61,10 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
initPreferences() initPreferences()
} }
override fun onPreferenceClick(preference: Preference?): Boolean { override fun onPreferenceClick(preference: Preference): Boolean {
val context = context ?: return false val context = context ?: return false
with(preference) { with(preference) {
this ?: return false
when (key) { when (key) {
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = File(context.cacheDir, "imageCache")
@@ -119,7 +117,9 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
if (!metadataFile.exists()) return@forEach if (!metadataFile.exists()) return@forEach
val metadata = metadataFile.readText()?.let { val metadata = metadataFile.readText()?.let {
runCatching {
json.decodeFromString<Metadata>(it) json.decodeFromString<Metadata>(it)
}.getOrNull()
} ?: return@forEach } ?: return@forEach
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach

View File

@@ -80,10 +80,8 @@ class SettingsFragment :
} }
} }
override fun onPreferenceClick(preference: Preference?): Boolean { override fun onPreferenceClick(preference: Preference): Boolean {
with (preference) { with (preference) {
this ?: return false
when (key) { when (key) {
"app_version" -> { "app_version" -> {
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
@@ -122,10 +120,8 @@ class SettingsFragment :
return true return true
} }
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
with (preference) { with (preference) {
this ?: return false
when (key) { when (key) {
"tag_translation" -> { "tag_translation" -> {
updateTranslations() updateTranslations()
@@ -163,7 +159,7 @@ class SettingsFragment :
when (key) { when (key) {
"proxy" -> { "proxy" -> {
summary = context?.let { getProxyInfo().type.name } summary = context.let { getProxyInfo().type.name }
} }
"download_folder" -> { "download_folder" -> {
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
@@ -300,7 +296,7 @@ class SettingsFragment :
} }
"oss" -> { "oss" -> {
setOnPreferenceClickListener { setOnPreferenceClickListener {
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
true true
} }
} }

View File

@@ -35,9 +35,14 @@ import xyz.quaver.pupil.client
import xyz.quaver.pupil.hitomi.* import xyz.quaver.pupil.hitomi.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@Serializable
data class OldReader(
val code: String,
val galleryInfo: OldGalleryInfo
)
@Serializable @Serializable
data class OldGalleryInfo( data class OldGalleryInfo(
val language_localname: String? = null, val language_localname: String? = null,
@@ -63,7 +68,7 @@ data class OldGalleryFiles(
@Serializable @Serializable
data class OldMetadata( data class OldMetadata(
var galleryBlock: GalleryBlock? = null, var galleryBlock: GalleryBlock? = null,
var reader: OldGalleryInfo? = null, var reader: OldReader? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } }) fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
@@ -75,12 +80,36 @@ data class Metadata(
var galleryInfo: GalleryInfo? = null, var galleryInfo: GalleryInfo? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
constructor(old: OldMetadata) : this(old.galleryBlock, getGalleryInfo(old.galleryBlock?.id ?: throw Exception()), old.imageList) constructor(old: OldMetadata) : this(
old.galleryBlock,
old.reader?.galleryInfo?.let { oldGalleryInfo ->
GalleryInfo(
oldGalleryInfo.id.toString(),
oldGalleryInfo.title ?: "",
null,
oldGalleryInfo.language,
oldGalleryInfo.type ?: "",
oldGalleryInfo.date ?: "",
files = oldGalleryInfo.files.map {
GalleryFiles(
it.width,
it.hash,
it.haswebp,
it.name,
it.height,
it.hasavif,
it.hasavifsmalltn
)
}
)
},
old.imageList
)
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } }) fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
} }
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) { class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object { companion object {
val instances = ConcurrentHashMap<Int, Cache>() val instances = ConcurrentHashMap<Int, Cache>()
@@ -103,7 +132,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
var metadata = kotlin.runCatching { var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let { metadata -> findFile(".metadata")?.readText()?.let { metadata ->
kotlin.runCatching { kotlin.runCatching {
json.decodeFromString<Metadata>(metadata) Json.decodeFromString<Metadata>(metadata)
}.getOrElse { }.getOrElse {
Metadata(json.decodeFromString<OldMetadata>(metadata)) Metadata(json.decodeFromString<OldMetadata>(metadata))
} }

View File

@@ -18,11 +18,21 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.pupil.R
import xyz.quaver.pupil.hitomi.GalleryBlock import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.imageUrlFromImage import xyz.quaver.pupil.hitomi.imageUrlFromImage
@@ -133,3 +143,30 @@ fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
val JsonElement.content val JsonElement.content
get() = this.jsonPrimitive.contentOrNull get() = this.jsonPrimitive.contentOrNull
fun checkNotificationEnabled(context: Context) =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
fun showNotificationPermissionExplanationDialog(context: Context) {
AlertDialog.Builder(context)
.setTitle(R.string.warning)
.setMessage(R.string.notification_denied)
.setPositiveButton(android.R.string.ok) { _, _ -> }
.show()
}
fun requestNotificationPermission(
activity: Activity,
requestPermissionLauncher: ActivityResultLauncher<String>,
showRationale: Boolean = true,
ifGranted: () -> Unit,
) {
when {
checkNotificationEnabled(activity) -> ifGranted()
showRationale && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS) ->
showNotificationPermissionExplanationDialog(activity)
else ->
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}

View File

@@ -27,17 +27,14 @@ import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@@ -138,6 +135,11 @@ fun checkUpdate(context: Context, force: Boolean = false) {
val msg = extractReleaseNote(update, Locale.getDefault()) val msg = extractReleaseNote(update, Locale.getDefault())
setMessage(Markwon.create(context).toMarkdown(msg)) setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
if (!checkNotificationEnabled(context)) {
showNotificationPermissionExplanationDialog(context)
return@setPositiveButton
}
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
//Cancel any download queued before //Cancel any download queued before
@@ -173,7 +175,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) { fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Int) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -191,9 +193,20 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let { val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]")
favorites.addAll(it)
onSuccess?.invoke(it) when (data) {
is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int })
is JsonObject -> {
val newFavorites = data["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
val newFavoriteTags = data["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
favorites.addAll(newFavorites)
favoriteTags.addAll(newFavoriteTags)
onSuccess?.invoke(favorites.size + favoriteTags.size)
}
else -> error("data is neither JsonArray or JsonObject")
} }
}.onFailure { onFailure?.invoke(it) } }.onFailure { onFailure?.invoke(it) }
} }

View File

@@ -22,6 +22,7 @@
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string> <string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string> <string name="settings_clear_history_summary">履歴数: %1$d</string>
<string name="main_drawer_history">履歴</string> <string name="main_drawer_history">履歴</string>
<string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
<string name="main_drawer_home">トップ</string> <string name="main_drawer_home">トップ</string>
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string> <string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
<string name="settings_security_mode_title">セキュリティーモード</string> <string name="settings_security_mode_title">セキュリティーモード</string>

View File

@@ -21,6 +21,7 @@
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string> <string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string> <string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
<string name="main_drawer_history">기록</string> <string name="main_drawer_history">기록</string>
<string name="notification_denied">백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다.</string>
<string name="main_drawer_home"></string> <string name="main_drawer_home"></string>
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string> <string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string> <string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>

View File

@@ -51,6 +51,8 @@
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string> <string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
<string name="notification_denied">Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled.</string>
<string name="main_drawer_home">Home</string> <string name="main_drawer_home">Home</string>
<string name="main_drawer_history">History</string> <string name="main_drawer_history">History</string>
<string name="main_drawer_downloads">Downloads</string> <string name="main_drawer_downloads">Downloads</string>

View File

@@ -18,6 +18,7 @@
--> -->
<paths> <paths>
<external-path name="external" path="/"/> <external-path name="external" path="."/>
<external-files-path name="files" path="/"/> <external-files-path name="files" path="."/>
<files-path name="files" path="." />
</paths> </paths>

View File

@@ -6,16 +6,16 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.0' classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.gms:google-services:4.3.10" classpath "com.google.gms:google-services:4.3.15"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1" classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9"
classpath "com.google.firebase:perf-plugin:1.4.1" classpath "com.google.firebase:perf-plugin:1.4.2"
classpath "com.google.android.gms:oss-licenses-plugin:0.10.4" classpath "com.google.android.gms:oss-licenses-plugin:0.10.6"
} }
} }

View File

@@ -20,4 +20,4 @@ kotlin.code.style=official
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
kotlin_version=1.6.10 kotlin_version=1.9.0

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

0
gradlew vendored Executable file → Normal file
View File