Compare commits

..

6 Commits

Author SHA1 Message Date
tom5079
65fafa8f2c Fixed typo & Built apk 2020-11-26 22:36:12 +09:00
tom5079
edef2a3eae Fixed show extra tags button not showing up & version up 2020-11-26 22:16:51 +09:00
tom5079
338241d26f Implemented proper Page Turn without relying on RecyclerView 2020-11-26 22:00:35 +09:00
tom5079
17b031e1b7 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	app/build.gradle
2020-11-16 16:13:02 +09:00
tom5079
2abfe1a061 ProgressCard 2020-11-16 16:11:57 +09:00
tom5079
7510202f55 fixed gallery import 2020-11-08 18:11:56 +09:00
142 changed files with 2664 additions and 6021 deletions

47
.gitignore vendored
View File

@@ -1,33 +1,20 @@
# 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
.idea/ .gradle
misc.xml /local.properties
deploymentTargetDropDown.xml /.idea/caches
render.experimental.xml /.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
# Keystore files #Github pages
*.jks /gh-pages
*.keystore
# Google Services (e.g. APIs or Firebase) #Private files
google-services.json **/google-services.json
**/credentials.json
# Android Profiling
*.hprof

139
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,139 @@
<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>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

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

6
.idea/compiler.xml generated Normal file
View File

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

6
.idea/copyright/GPL.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<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>

7
.idea/copyright/profiles_settings.xml generated Normal file
View File

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

7
.idea/dictionaries/tom50.xml generated Normal file
View File

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

4
.idea/encodings.xml generated Normal file
View File

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

21
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,21 @@
<?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="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<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>

75
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,75 @@
<?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>
</component>
</project>

7
.idea/kotlinCodeInsightSettings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?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 Normal file
View File

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

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" 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>

12
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

3
.idea/scopes/Pupil.xml generated Normal file
View File

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

6
.idea/vcs.xml generated Normal file
View File

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

View File

@@ -1,29 +1,26 @@
![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true) ![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true)
*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.18/Pupil-v5.3.18.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.18/Pupil-v5.3.18.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.1.6-hotfix7/Pupil-v5.1.6-hotfix7.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.1.6-hotfix7/Pupil-v5.1.6-hotfix7.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
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.jpg?raw=true) ![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.jpg?raw=true)
# Installation # Installation
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean) Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
or Build app yourself or Build app yourself
# Manual # Manual
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. [Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
Consider using translator.
# Contribution # Contribution
Any kind of contribution is appreciated. Feel free to leave PR! Any kind of contribution is appriciated. Feel free to leave PR!
## Tag Translation ## Tag Translation
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags) Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)

139
app/build.gradle Normal file
View File

@@ -0,0 +1,139 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-kapt"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlinx-serialization"
apply plugin: "com.google.android.gms.oss-licenses-plugin"
if (file("google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
apply plugin: "com.google.gms.google-services"
apply plugin: "com.google.firebase.crashlytics"
apply plugin: "com.google.firebase.firebase-perf"
} else {
logger.lifecycle("Firebase Disabled")
}
ext {
okhttp_version = "3.12.12"
}
configurations {
all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") {
// OkHttp drops support before 5.0 since 3.13.0
details.useVersion okhttp_version
}
}
}
}
}
android {
compileSdkVersion 30
defaultConfig {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 30
versionCode 64
versionName "5.1.7-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
debug {
minifyEnabled true
shrinkResources true
debuggable true
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
ext.enableCrashlytics = false
ext.alwaysUpdateBuildId = false
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion = "29.0.3"
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.activity:activity-ktx:1.2.0-beta01"
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.0.1"
implementation "androidx.work:work-runtime-ktx:2.4.0"
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0-alpha04"
implementation "com.google.firebase:firebase-core:18.0.0"
implementation "com.google.firebase:firebase-analytics:18.0.0"
implementation "com.google.firebase:firebase-crashlytics:17.3.0"
implementation "com.google.firebase:firebase-perf:19.0.10"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1"
implementation "com.github.clans:fab:1.6.4"
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
implementation 'com.github.piasy:BigImageViewer:1.6.7'
implementation 'com.github.piasy:FrescoImageLoader:1.6.7'
implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
implementation "net.rdrei.android.dirchooser:library:3.2@aar"
implementation "com.gu:option:1.3"
implementation "com.andrognito.patternlockview:patternlockview:1.0.0"
//implementation "com.andrognito.pinlockview:pinlockview:2.1.0"
implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.8.16"
implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.0.7"
testImplementation "junit:junit:4.13.1"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.test:rules:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
}
androidExtensions {
experimental = true
}

View File

@@ -1,144 +0,0 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.gms.oss.licenses)
alias(libs.plugins.gms.google.services)
alias(libs.plugins.firebase.crashlytics)
alias(libs.plugins.firebase.perf)
id("kotlin-parcelize")
}
android {
namespace = "xyz.quaver.pupil"
compileSdk = 35
defaultConfig {
applicationId = "xyz.quaver.pupil"
minSdk = 21
targetSdk = 35
versionCode = 70
versionName = "5.3.18"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildFeatures {
buildConfig = true
compose = true
}
buildTypes {
debug {
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
extra.apply {
set("enableCrashlytics", false)
set("alwaysUpdateBuildId", false)
}
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(platform(libs.compose.bom))
androidTestImplementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
androidTestImplementation(libs.compose.ui.test)
debugImplementation(libs.compose.ui.test.manifest)
implementation(libs.compose.icons)
implementation(libs.compose.adaptive)
implementation(libs.compose.activity)
implementation(libs.compose.lifecycle.viewmodel)
implementation(libs.compose.livedata)
implementation(libs.compose.navigation)
implementation(libs.accompanist.adaptive)
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.activity.ktx)
implementation(libs.fragment.ktx)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
implementation(libs.constraintlayout)
implementation(libs.gridlayout)
implementation(libs.biometric)
implementation(libs.work.runtime.ktx)
implementation(libs.library)
implementation(libs.material)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics.ktx)
implementation(libs.firebase.crashlytics.ktx)
implementation(libs.firebase.perf.ktx)
implementation(libs.play.services.oss.licenses)
implementation(libs.play.services.mlkit.face.detection)
implementation(libs.fab)
implementation(libs.bigimageviewer)
implementation(libs.frescoimageloader)
implementation(libs.frescoimageviewfactory)
implementation(libs.imagepipeline.okhttp3)
//noinspection GradleDependency
implementation(libs.okhttp)
implementation(libs.ktor.network)
implementation(libs.dotsindicator)
implementation(libs.pinlockview)
implementation(libs.patternlockview)
implementation(libs.core)
implementation(libs.ripplebackground.library)
implementation(libs.recyclerview.fastscroller)
implementation(libs.jsoup)
implementation(libs.documentfilex)
implementation(libs.floatingsearchview)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.rules)
androidTestImplementation(libs.runner)
androidTestImplementation(libs.espresso.core)
}

View File

@@ -32,6 +32,4 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment -keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment -keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
-keep class xyz.quaver.pupil.** { *; }
-keep class app.cash.zipline.** { *; }

View File

@@ -1,37 +1,18 @@
{ {
"version": 3, "version": 2,
"artifactType": { "artifactType": {
"type": "APK", "type": "APK",
"kind": "Directory" "kind": "Directory"
}, },
"applicationId": "xyz.quaver.pupil", "applicationId": "xyz.quaver.pupil",
"variantName": "release", "variantName": "processReleaseResources",
"elements": [ "elements": [
{ {
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "versionCode": 64,
"versionCode": 70, "versionName": "5.1.7-beta1",
"versionName": "5.3.18",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ]
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 21
} }

View File

@@ -20,18 +20,10 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.pupil.hitomi.*
import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@@ -40,144 +32,10 @@ import java.util.concurrent.TimeUnit
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
// @Before
// fun init() {
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// }
@Before
fun init() {
clientBuilder = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.SECONDS)
.writeTimeout(0, TimeUnit.SECONDS)
.callTimeout(0, TimeUnit.SECONDS)
.connectTimeout(0, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("Referer", "https://hitomi.la/")
.build()
chain.proceed(request)
}
}
@Test @Test
fun test_empty() { fun useAppContext() {
print( // Context of the app under test.
"".trim() val appContext = InstrumentationRegistry.getInstrumentation().targetContext
.replace(Regex("""^\?"""), "")
.lowercase(Locale.getDefault())
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
})
} }
@Test
fun test_nozomi() {
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
Log.d("PUPILD", nozomi.size.toString())
}
@Test
fun test_search() {
val ids = getGalleryIDsForQuery("language:korean").reversed()
print(ids.size)
}
@Test
fun test_suggestions() {
val suggestions = getSuggestionsForQuery("language:g")
print(suggestions)
}
@Test
fun test_doSearch() {
val r = runBlocking {
doSearch("language:korean")
}
Log.d("PUPILD", r.take(10).toString())
}
// @Test
// fun test_getBlock() {
// val galleryBlock = getGalleryBlock(2097576)
//
// print(galleryBlock)
// }
//
// @Test
// fun test_getGallery() {
// val gallery = getGallery(2097751)
//
// print(gallery)
// }
@Test
fun test_getGalleryInfo() {
val info = getGalleryInfo(1469394)
print(info)
}
@Test
fun test_getReader() {
val reader = getGalleryInfo(2128654)
Log.d("PUPILD", reader.toString())
}
@Test
fun test_getImages() { runBlocking {
val galleryID = 2128654
val images = getGalleryInfo(galleryID).files.map {
imageUrlFromImage(galleryID, it,false)
}
Log.d("PUPILD", images.toString())
// images.forEachIndexed { index, image ->
// println("Testing $index/${images.size}: $image")
// val response = client.newCall(
// Request.Builder()
// .url(image)
// .header("Referer", "https://hitomi.la/")
// .build()
// ).execute()
//
// assertEquals(200, response.code())
//
// println("$index/${images.size} Passed")
// }
} }
// @Test
// fun test_urlFromUrlFromHash() {
// val url = urlFromUrlFromHash(1531795, GalleryFiles(
// 212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
// ), "webp")
//
// print(url)
// }
// @Test
// suspend fun test_doSearch_extreme() {
// val query = "language:korean -tag:sample -female:humiliation -female:diaper -female:strap-on -female:squirting -female:lizard_girl -female:voyeurism -type:artistcg -female:blood -female:ryona -male:blood -male:ryona -female:crotch_tattoo -male:urethra_insertion -female:living_clothes -male:tentacles -female:slave -female:gag -male:gag -female:wooden_horse -male:exhibitionism -male:miniguy -female:mind_break -male:mind_break -male:unbirth -tag:scanmark -tag:no_penetration -tag:nudity_only -female:enema -female:brain_fuck -female:navel_fuck -tag:novel -tag:mosaic_censorship -tag:webtoon -male:rape -female:rape -female:yuri -male:anal -female:anal -female:futanari -female:huge_breasts -female:big_areolae -male:torture -male:stuck_in_wall -female:stuck_in_wall -female:torture -female:birth -female:pregnant -female:drugs -female:bdsm -female:body_writing -female:cbt -male:dark_skin -male:insect -female:insect -male:vore -female:vore -female:vomit -female:urination -female:urethra_insertion -tag:mmf_threesome -female:sex_toys -female:double_penetration -female:eggs -female:prolapse -male:smell -male:bestiality -female:bestiality -female:big_ass -female:milf -female:mother -male:dilf -male:netorare -female:netorare -female:cosplaying -female:filming -female:armpit_sex -female:armpit_licking -female:tickling -female:lactation -male:skinsuit -female:skinsuit -male:bbm -female:prostitution -female:double_penetration -female:females_only -male:males_only -female:tentacles -female:tentacles -female:stomach_deformation -female:hairy_armpits -female:large_insertions -female:mind_control -male:orc -female:dark_skin -male:yandere -female:yandere -female:scat -female:toddlercon -female:bbw -female:hairy -male:cuntboy -male:lactation -male:drugs -female:body_modification -female:monoeye -female:chikan -female:long_tongue -female:harness -female:fisting -female:glory_hole -female:latex -male:latex -female:unbirth -female:giantess -female:sole_dickgirl -female:robot -female:doll_joints -female:machine -tag:artbook -male:cbt -female:farting -male:farting -male:midget -female:midget -female:exhibitionism -male:monster -female:big_nipples -female:big_clit -female:gyaru -female:piercing -female:necrophilia -female:snuff -female:smell -male:cheating -female:cheating -male:snuff -female:harem -male:harem"
// print(doSearch(query).size)
// }
// @Test
// suspend fun test_parse() {
// print(doSearch("-male:yaoi -female:yaoi -female:loli").size)
// }
// @Test
// fun test_subdomainFromUrl() {
// val galleryInfo = getGalleryInfo(1929109).files[2]
// print(urlFromUrlFromHash(1929109, galleryInfo, "webp", null, "a"))
// }
} }

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079 ~ Copyright (C) 2020 tom5079
~ ~
@@ -16,4 +17,6 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>. ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<resources></resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
</resources>

View File

@@ -1,38 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission <uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
android:maxSdkVersion="23" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="23" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32"
tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-feature <uses-feature android:name="android.hardware.camera" android:required="false" />
android:name="android.hardware.camera" <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -40,20 +21,20 @@
android:fullBackupContent="true" android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute" android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme"> tools:replace="android:theme"
tools:ignore="UnusedAttribute">
<meta-data <meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES" android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="face" /> android:value="face" />
<provider <provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
@@ -63,20 +44,8 @@
</provider> </provider>
<service <service android:name=".services.DownloadService"
android:name=".services.DownloadService" android:exported="false"/>
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".services.TransferClientService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".services.TransferServerService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<receiver <receiver
android:name=".receiver.UpdateBroadcastReceiver" android:name=".receiver.UpdateBroadcastReceiver"
@@ -90,123 +59,190 @@
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true" android:parentActivityName=".ui.MainActivity">
android:parentActivityName=".ui.MainComposeActivity"> <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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="*.hasha.in" /> android:pathPrefix="/galleries"
<data android:pathPrefix="/reader" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/manga"
<data android:pathPrefix="/galleries" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/doujinshi"
<data android:pathPrefix="/manga" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/cg"
<data android:pathPrefix="/doujinshi" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/reader"
<data android:pathPrefix="/cg" /> android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/galleries"
<data android:pathPrefix="/imageset" /> android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:scheme="https" /> android:host="hitomi.la"
<data android:host="hitomi.la" /> android:pathPrefix="/manga"
<data android:pathPrefix="/reader" /> android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="http" /> <data
<data android:host="e-hentai.org" /> android:host="hitomi.la"
<data android:pathPrefix="/g" /> android:pathPrefix="/doujinshi"
android:scheme="https" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true"> <intent-filter>
<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 android:scheme="https" /> <data
<data android:host="e-hentai.org" /> android:host="hitomi.la"
<data android:pathPrefix="/g" /> android:pathPrefix="/cg"
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
android:name=".ui.MainComposeActivity" android:name=".ui.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true" android:theme="@style/NoActionBarAppTheme">
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<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=".ui.TransferActivity" /> <activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
</application> </application>
</manifest> </manifest>

View File

@@ -29,35 +29,24 @@ import android.os.Build
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 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
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.FirebaseApp import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import okhttp3.Dispatcher
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.SavedSet import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.getProxyInfo import xyz.quaver.setClient
import xyz.quaver.pupil.util.preferences
import xyz.quaver.pupil.util.proxyInfo
import java.io.File import java.io.File
import java.security.KeyStore import java.util.*
import java.security.SecureRandom
import java.security.cert.CertificateFactory
import java.util.UUID
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.reflect.KClass import kotlin.reflect.KClass
typealias PupilInterceptor = (Interceptor.Chain) -> Response typealias PupilInterceptor = (Interceptor.Chain) -> Response
@@ -79,91 +68,39 @@ var clientHolder: OkHttpClient? = null
val client: OkHttpClient val client: OkHttpClient
get() = clientHolder ?: clientBuilder.build().also { get() = clientHolder ?: clientBuilder.build().also {
clientHolder = it clientHolder = it
setClient(it)
} }
fun getSSLContext(context: Context): SSLContext {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
val certificateFactory = CertificateFactory.getInstance("X.509")
val certificate = context.resources.openRawResource(R.raw.isrgrootx1).use {
certificateFactory.generateCertificate(it)
}
keyStore.setCertificateEntry("isrgrootx1", certificate)
val defaultTrustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
defaultTrustManagerFactory.init(null as KeyStore?)
defaultTrustManagerFactory.trustManagers.filterIsInstance<X509TrustManager>()
.forEach { trustManager ->
trustManager.acceptedIssuers.forEach { acceptedIssuer ->
keyStore.setCertificateEntry(acceptedIssuer.subjectDN.name, acceptedIssuer)
}
}
val trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(keyStore)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustManagerFactory.trustManagers, SecureRandom())
return sslContext
}
class Pupil : Application() { class Pupil : Application() {
companion object {
lateinit var instance: Pupil
private set
}
override fun onCreate() { override fun onCreate() {
instance = this
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
val userID = Preferences["user_id", ""].let { userID -> val userID = Preferences["user_id", ""].let { userID ->
if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it } if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
else userID else userID
} }
FirebaseApp.initializeApp(this)
FirebaseCrashlytics.getInstance().setUserId(userID) FirebaseCrashlytics.getInstance().setUserId(userID)
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder() clientBuilder = OkHttpClient.Builder()
// .connectTimeout(0, TimeUnit.SECONDS) .connectTimeout(0, TimeUnit.SECONDS)
.sslSocketFactory(getSSLContext(this).socketFactory)
.readTimeout(0, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS)
.proxyInfo(proxyInfo) .proxyInfo(proxyInfo)
.addInterceptor { chain -> .addInterceptor { chain ->
val request = chain.request().newBuilder() val request = chain.request()
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
)
.header("Referer", "https://hitomi.la/")
.build()
val tag = request.tag() ?: return@addInterceptor chain.proceed(request) val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request) interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
}.apply {
(Preferences.get<String>("max_concurrent_download").toIntOrNull() ?: 0).let {
if (it != 0)
dispatcher(Dispatcher(Executors.newFixedThreadPool(it)))
}
} }
try { try {
Preferences.get<String>("download_folder").also { Preferences.get<String>("download_folder").also {
if (it.startsWith("content://")) if (it.startsWith("content") && Build.VERSION.SDK_INT > 19)
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
Uri.parse(it), Uri.parse(it),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -171,6 +108,8 @@ class Pupil : Application() {
if (!FileX(this, it).canWrite()) if (!FileX(this, it).canWrite())
throw Exception() throw Exception()
DownloadManager.getInstance(this).migrate()
} }
} catch (e: Exception) { } catch (e: Exception) {
Preferences.remove("download_folder") Preferences.remove("download_folder")
@@ -183,17 +122,11 @@ class Pupil : Application() {
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0) histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0) favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
favoriteTags = favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "") searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
favoriteTags.filter { it.tag.contains('_') }.forEach {
favoriteTags.remove(it)
}
/*
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)*/ FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
@@ -203,85 +136,44 @@ class Pupil : Application() {
e.printStackTrace() e.printStackTrace()
} }
BigImageViewer.initialize( BigImageViewer.initialize(FrescoImageLoader.with(this))
FrescoImageLoader.with(
this,
OkHttpImagePipelineConfigFactory
.newBuilder(this, client)
.build()
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel( manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
NotificationChannel( description = getString(R.string.channel_download_description)
"download", enableLights(false)
getString(R.string.channel_download), enableVibration(false)
NotificationManager.IMPORTANCE_LOW lockscreenVisibility = Notification.VISIBILITY_SECRET
).apply { })
description = getString(R.string.channel_download_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel( manager.createNotificationChannel(NotificationChannel("downloader", getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW).apply {
NotificationChannel( description = getString(R.string.channel_downloader_description)
"downloader", enableLights(false)
getString(R.string.channel_downloader), enableVibration(false)
NotificationManager.IMPORTANCE_LOW lockscreenVisibility = Notification.VISIBILITY_SECRET
).apply { })
description = getString(R.string.channel_downloader_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel( manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
NotificationChannel( description = getString(R.string.channel_update_description)
"update", enableLights(true)
getString(R.string.channel_update), enableVibration(true)
NotificationManager.IMPORTANCE_HIGH lockscreenVisibility = Notification.VISIBILITY_SECRET
).apply { })
description = getString(R.string.channel_update_description)
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel( manager.createNotificationChannel(NotificationChannel("import", getString(R.string.channel_update), NotificationManager.IMPORTANCE_LOW).apply {
NotificationChannel( description = getString(R.string.channel_update_description)
"import", enableLights(false)
getString(R.string.channel_update), enableVibration(false)
NotificationManager.IMPORTANCE_LOW lockscreenVisibility = Notification.VISIBILITY_SECRET
).apply { })
description = getString(R.string.channel_update_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
manager.createNotificationChannel(
NotificationChannel(
"transfer",
getString(R.string.channel_transfer),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.channel_transfer_description)
enableLights(false)
enableVibration(false)
lockscreenVisibility = Notification.VISIBILITY_SECRET
})
} }
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
when (Preferences.get<Boolean>("dark_mode")) { true -> AppCompatDelegate.MODE_NIGHT_YES
true -> AppCompatDelegate.MODE_NIGHT_YES false -> AppCompatDelegate.MODE_NIGHT_NO
false -> AppCompatDelegate.MODE_NIGHT_NO })
}
)
super.onCreate() super.onCreate()
} }

View File

@@ -25,6 +25,7 @@ import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -34,14 +35,15 @@ import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.android.synthetic.main.view_progress_card.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.hitomi.getGallery
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.GalleryblockItemBinding
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.hitomi.getGallery
import xyz.quaver.pupil.hitomi.getGalleryInfo
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.ProgressCard import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
@@ -55,20 +57,20 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
var updateAll = true var updateAll = true
var thin: Boolean = Preferences["thin"] var thin: Boolean = Preferences["thin"]
inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private var galleryID: Int = 0 private var galleryID: Int = 0
init { init {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
while (updateAll) { while (updateAll) {
updateProgress(itemView.context) updateProgress(view.context)
delay(1000) delay(1000)
} }
} }
} }
private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch { private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
with(binding.galleryblockCard) { with(view.galleryblock_card) {
val imageList = Cache.getInstance(context, galleryID).metadata.imageList val imageList = Cache.getInstance(context, galleryID).metadata.imageList
if (imageList == null) { if (imageList == null) {
@@ -79,7 +81,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
progress = imageList.count { it != null } progress = imageList.count { it != null }
max = imageList.size max = imageList.size
this@GalleryViewHolder.binding.galleryblockId.setOnClickListener { view.galleryblock_id.setOnClickListener {
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("gallery_id", galleryID.toString()) ClipData.newPlainText("gallery_id", galleryID.toString())
) )
@@ -100,175 +102,177 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
fun bind(galleryID: Int) { fun bind(galleryID: Int) {
this.galleryID = galleryID this.galleryID = galleryID
updateProgress(itemView.context) updateProgress(view.context)
val cache = Cache.getInstance(itemView.context, galleryID) val cache = Cache.getInstance(view.context, galleryID)
CoroutineScope(Dispatchers.IO).launch { val galleryBlock = runBlocking {
val galleryBlock = cache.getGalleryBlock() ?: return@launch cache.getGalleryBlock()
} ?: return
launch(Dispatchers.Main) { with(view) {
val resources = itemView.context.resources val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map { val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split -> it.split("|").let { split ->
Pair(split[0], split[1]) Pair(split[0], split[1])
}
}.toMap()
val artists = galleryBlock.artists
val series = galleryBlock.series
binding.galleryblockThumbnail.apply {
setOnClickListener {
itemView.performClick()
}
setOnLongClickListener {
itemView.performLongClick()
}
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
setImageLoaderCallback(object: ImageLoader.Callback {
override fun onFail(error: Exception?) {
Cache.delete(context, galleryID)
}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
}
} }
}.toMap()
binding.galleryblockTitle.text = galleryBlock.title val artists = galleryBlock.artists
with(binding.galleryblockArtist) { val series = galleryBlock.series
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch { galleryblock_thumbnail.apply {
val gallery = runCatching { setOnClickListener {
getGallery(galleryID) view.performClick()
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
} }
with(binding.galleryblockSeries) { setOnLongClickListener {
text = view.performLongClick()
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
} }
binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize() setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
with(binding.galleryblockLanguage) { setImageLoaderCallback(object: ImageLoader.Callback {
text = override fun onFail(error: Exception?) {
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) Cache.getInstance(context, galleryID).let { cache ->
visibility = when { cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() }
!galleryBlock.language.isNullOrEmpty() -> View.VISIBLE cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() }
else -> View.GONE
}
}
with(binding.galleryblockTagGroup) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
} }
} }
tags.clear() override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
CoroutineScope(Dispatchers.IO).launch { override fun onFinish() {}
tags.addAll( override fun onProgress(progress: Int) {}
galleryBlock.relatedTags.sortedBy { override fun onStart() {}
val tag = Tag.parse(it) override fun onSuccess(image: File?) {}
})
if (favoriteTags.contains(tag)) ssiv?.recycle()
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
launch(Dispatchers.Main) {
refresh()
}
}
}
binding.galleryblockId.text = galleryBlock.id.toString()
binding.galleryblockPagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching { cache.getThumbnail().let { launch(Dispatchers.Main) {
getGalleryInfo(galleryBlock.id).files.size showImage(it)
}.getOrNull() ?: return@launch } }
withContext(Dispatchers.Main) { }
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount) }
galleryblock_title.text = galleryBlock.title
with(galleryblock_artist) {
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch {
val gallery = runCatching {
getGallery(galleryID)
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
}
with(galleryblock_series) {
text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(galleryblock_language) {
text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(galleryblock_tag_group) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
} }
} }
with(binding.galleryblockFavorite) { tags.clear()
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty) CoroutineScope(Dispatchers.IO).launch {
} tags.addAll(
else -> { galleryBlock.relatedTags.sortedBy {
favorites.add(galleryBlock.id) val tag = Tag.parse(it)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply { if (favoriteTags.contains(tag))
this ?: return@apply -1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() { launch(Dispatchers.Main) {
override fun onAnimationEnd(drawable: Drawable?) { refresh()
setImageResource(R.drawable.ic_star_filled) }
} }
}) }
start()
galleryblock_id.text = galleryBlock.id.toString()
galleryblock_pagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getReader(galleryBlock.id).galleryInfo.files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(galleryblock_favorite) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
}
else -> {
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
}
}) })
} start()
})
} }
} }
} }
} }
}
// Make some views invisible to make it thinner
if (thin) { // Make some views invisible to make it thinner
binding.galleryblockTagGroup.visibility = View.GONE if (thin) {
galleryblock_tag_group.visibility = View.GONE
}
} }
} }
} }
@@ -278,7 +282,9 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
var onDeleteClickedHandler: ((Int) -> Unit)? = null var onDeleteClickedHandler: ((Int) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) return GalleryViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_galleryblock, parent, false)
)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@@ -287,25 +293,25 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
holder.bind(galleryID) holder.bind(galleryID)
holder.binding.galleryblockCard.binding.download.setOnClickListener { holder.view.galleryblock_card.download.setOnClickListener {
onDownloadClickedHandler?.invoke(position) onDownloadClickedHandler?.invoke(position)
} }
holder.binding.galleryblockCard.binding.delete.setOnClickListener { holder.view.galleryblock_card.delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position) onDeleteClickedHandler?.invoke(position)
} }
mItemManger.bindView(holder.binding.root, position) mItemManger.bindView(holder.view, position)
holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener { holder.view.galleryblock_card.swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) { override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout) mItemManger.closeAllExcept(layout)
holder.binding.galleryblockCard.binding.download.text = holder.view.galleryblock_card.download.text =
if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID)) if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID))
holder.binding.root.context.getString(android.R.string.cancel) holder.view.context.getString(android.R.string.cancel)
else else
holder.binding.root.context.getString(R.string.main_download) holder.view.context.getString(R.string.main_download)
} }
override fun onClose(layout: SwipeLayout?) {} override fun onClose(layout: SwipeLayout?) {}

View File

@@ -0,0 +1,86 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_mirrors.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.Preferences
import java.util.*
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
it.split('|').let { split ->
Pair(split.first(), split.last())
}
}.toMap()
val list = mirrors.keys.toMutableList().apply {
Preferences.get<String>("mirrors")
.split(">")
.reversed()
.forEach {
if (this.contains(it)) {
this.remove(it)
this.add(0, it)
}
}
}
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
Collections.swap(list, from, to)
notifyItemMoved(from, to)
onItemMoved?.invoke(list)
}
var onStartDrag : ((ViewHolder) -> Unit)? = null
var onItemMoved : ((List<String>) -> (Unit))? = null
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.view) {
mirror_name.text = mirrors[list.elementAt(position)]
mirror_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
onStartDrag?.invoke(holder)
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_mirrors, parent, false
).let {
ViewHolder(it)
}
}
override fun getItemCount() = mirrors.size
}

View File

@@ -39,10 +39,10 @@ import com.facebook.imagepipeline.image.ImageInfo
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import com.github.piasy.biv.view.ImageShownCallback import com.github.piasy.biv.view.ImageShownCallback
import com.github.piasy.biv.view.ImageViewFactory import com.github.piasy.biv.view.ImageViewFactory
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import java.io.File import java.io.File
@@ -52,93 +52,16 @@ class ReaderAdapter(
private val activity: ReaderActivity, private val activity: ReaderActivity,
private val galleryID: Int private val galleryID: Int
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var galleryInfo: GalleryInfo? = null
var reader: Reader? = null
var isFullScreen = false var isFullScreen = false
var onItemClickListener : (() -> (Unit))? = null var onItemClickListener : (() -> (Unit))? = null
inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) { class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
init {
with (binding.image) {
setImageViewFactory(FrescoImageViewFactory().apply {
updateView = { imageInfo ->
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
}
}
})
setImageShownCallback(object : ImageShownCallback {
override fun onMainImageShown() {
binding.image.mainView.let { v ->
when (v) {
is SubsamplingScaleImageView ->
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
}
override fun onThumbnailShown() {}
})
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
setOnClickListener {
onItemClickListener?.invoke()
}
}
binding.root.setOnClickListener {
onItemClickListener?.invoke()
}
}
fun bind(position: Int) {
if (cache == null)
cache = Cache.getInstance(itemView.context, galleryID)
if (!isFullScreen) {
binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0
dimensionRatio =
"${galleryInfo!!.files[position].width}:${galleryInfo!!.files[position].height}"
}
} else {
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_PARENT
dimensionRatio = null
}
binding.root.background = null
}
binding.readerIndex.text = (position+1).toString()
val image = cache!!.getImage(position)
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
if (progress?.isInfinite() == true && image != null) {
binding.progressGroup.visibility = View.INVISIBLE
binding.image.showImage(image.uri)
} else {
binding.progressGroup.visibility = View.VISIBLE
binding.readerItemProgressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
clear()
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
notifyItemChanged(position)
}
}
}
fun clear() { fun clear() {
binding.image.mainView.let { view.image.mainView.let {
when (it) { when (it) {
is SubsamplingScaleImageView -> is SubsamplingScaleImageView ->
it.recycle() it.recycle()
@@ -150,15 +73,91 @@ class ReaderAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false
).let {
with(it) {
image.setImageViewFactory(FrescoImageViewFactory().apply {
updateView = { imageInfo ->
it.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
}
}
})
image.setImageShownCallback(object : ImageShownCallback {
override fun onMainImageShown() {
it.image.mainView.let { v ->
when (v) {
is SubsamplingScaleImageView ->
if (!isFullScreen) it.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
}
override fun onThumbnailShown() {}
})
image.setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
image.setOnClickListener {
this.performClick()
}
setOnClickListener {
onItemClickListener?.invoke()
}
}
ViewHolder(it)
}
} }
private var cache: Cache? = null private var cache: Cache? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position) holder.view as ConstraintLayout
if (cache == null)
cache = Cache.getInstance(holder.view.context, galleryID)
if (!isFullScreen) {
holder.view.setBackgroundResource(R.drawable.reader_item_boundary)
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0
dimensionRatio =
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
} else {
holder.view.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_PARENT
dimensionRatio = null
}
holder.view.background = null
}
holder.view.reader_index.text = (position+1).toString()
val image = cache!!.getImage(position)
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
if (progress?.isInfinite() == true && image != null) {
holder.view.progress_group.visibility = View.INVISIBLE
holder.view.image.showImage(image.uri)
} else {
holder.view.progress_group.visibility = View.VISIBLE
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.clear()
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
notifyItemChanged(position)
}
}
} }
override fun getItemCount() = galleryInfo?.files?.size ?: 0 override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
holder.clear() holder.clear()

View File

@@ -1,44 +0,0 @@
package xyz.quaver.pupil.adapters
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.TransferPeerListItemBinding
class TransferPeersAdapter(
private val devices: Collection<WifiP2pDevice>,
private val onDeviceSelected: (WifiP2pDevice) -> Unit
): RecyclerView.Adapter<TransferPeersAdapter.ViewHolder>() {
class ViewHolder(val binding: TransferPeerListItemBinding): RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = TransferPeerListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val device = devices.elementAt(position)
holder.binding.deviceName.text = device.deviceName
holder.binding.deviceAddress.text = device.deviceAddress
holder.binding.root.setOnClickListener {
onDeviceSelected(device)
}
}
override fun getItemCount(): Int {
return devices.size
}
}

View File

@@ -1,273 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import xyz.quaver.pupil.client
import java.io.IOException
import java.net.URL
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime
const val protocol = "https:"
@Serializable
data class Artist(
val artist: String,
val url: String
)
@Serializable
data class Group(
val group: String,
val url: String
)
@Serializable
data class Parody(
val parody: String,
val url: String
)
@Serializable
data class Character(
val character: String,
val url: String
)
@Serializable
data class Tag(
val tag: String,
val url: String,
val female: String? = null,
val male: String? = null
)
@Serializable
data class Language(
val galleryid: String,
val url: String,
val language_localname: String,
val name: String
)
@Serializable
data class GalleryInfo(
val id: String,
val title: String,
val japanese_title: String? = null,
val language: String? = null,
val type: String,
val date: String,
val artists: List<Artist>? = null,
val groups: List<Group>? = null,
val parodys: List<Parody>? = null,
val tags: List<Tag>? = null,
val related: List<Int> = emptyList(),
val languages: List<Language> = emptyList(),
val characters: List<Character>? = null,
val scene_indexes: List<Int>? = emptyList(),
val files: List<GalleryFiles> = emptyList()
)
val json = Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
}
typealias HeaderSetter = (Request.Builder) -> Request.Builder
fun URL.readText(settings: HeaderSetter? = null): String {
val request = Request.Builder()
.url(this).let {
settings?.invoke(it) ?: it
}.build()
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.string() } ?: throw IOException()
}
fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
val request = Request.Builder()
.url(this).let {
settings?.invoke(it) ?: it
}.build()
return client.newCall(request).execute().also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw IOException()
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun getGalleryInfo(galleryID: Int) =
json.decodeFromString<GalleryInfo>(
URL("$protocol//$domain/galleries/$galleryID.js").readText()
.replace("var galleryinfo = ", "")
)
//common.js
const val domain = "ltn.hitomi.la"
const val galleryblockextension = ".html"
const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi"
val evaluationContext = Dispatchers.Main + Job()
object gg {
private var lastRetrieval: Long? = null
private val mutex = Mutex()
private var mDefault = 0
private val mMap = mutableMapOf<Int, Int>()
private var b = ""
@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)
}
override fun onResponse(call: Call, response: Response) {
if (!call.isCanceled) {
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)
}
}
suspend fun subdomainFromURL(url: String, base: String? = null) : String {
var retval = "b"
if (!base.isNullOrBlank())
retval = base
val b = 16
val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
val m = r.find(url) ?: return "a"
val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b)
if (g != null) {
retval = (97+ gg.m(g)).toChar().toString() + retval
}
return retval
}
suspend fun urlFromUrl(url: String, base: String? = null) : String {
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
}
suspend fun fullPathFromHash(hash: String) : String =
"${gg.b()}${gg.s(hash)}/$hash"
fun realFullPathFromHash(hash: String): String =
hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash")
suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' }
val dir = dir ?: "images"
return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
}
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
if (base == "tn")
urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base)
else
urlFromUrl(urlFromHash(galleryID, image, dir, ext), base)
suspend fun rewriteTnPaths(html: String) {
html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")) { url ->
runBlocking {
urlFromUrl(url.value, "tn")
}
}
}
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return urlFromUrlFromHash(galleryID, image, "webp", null, "a")
// return when {
// noWebp ->
// urlFromUrlFromHash(galleryID, image)
//// image.hasavif != 0 ->
//// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
// image.haswebp != 0 ->
// urlFromUrlFromHash(galleryID, image, "webp", null, "a")
// else ->
// urlFromUrlFromHash(galleryID, image)
// }
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
@Serializable
data class Gallery(
val related: List<Int>,
val langList: List<Pair<String, String>>,
val cover: String,
val title: String,
val artists: List<String>,
val groups: List<String>,
val type: String,
val language: String,
val series: List<String>,
val characters: List<String>,
val tags: List<String>,
val thumbnails: List<String>
)
suspend fun getGallery(galleryID: Int) : Gallery {
val info = getGalleryInfo(galleryID)
return Gallery(
info.related,
info.languages.map { it.name to it.galleryid },
urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn"),
info.title,
info.artists?.map { it.artist }.orEmpty(),
info.groups?.map { it.group }.orEmpty(),
info.type,
info.language.orEmpty(),
info.parodys?.map { it.parody }.orEmpty(),
info.characters?.map { it.character }.orEmpty(),
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
info.files.map { urlFromUrlFromHash(galleryID, it, "webpsmalltn", "webp", "tn") }
)
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.net.URL
import java.net.URLDecoder
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.net.ssl.HttpsURLConnection
import kotlin.io.readText
//galleryblock.js
fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair<List<Int>, Int> {
val url = when(area) {
null -> "$protocol//$domain/$tag-$language$nozomiextension"
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
}
with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET"
if (start != -1 && count != -1) {
val startByte = start*4
val endByte = (start+count)*4-1
setRequestProperty("Range", "bytes=$startByte-$endByte")
}
connect()
val totalItems = getHeaderField("Content-Range")
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
val nozomi = ArrayList<Int>()
val arrayBuffer = ByteBuffer
.wrap(inputStream.readBytes())
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return Pair(nozomi, totalItems)
}
}
@Serializable
data class GalleryBlock(
val id: Int,
val galleryUrl: String,
val thumbnails: List<String>,
val title: String,
val artists: List<String>,
val series: List<String>,
val type: String,
val language: String,
val relatedTags: List<String>,
val groups: List<String> = emptyList()
)
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
val info = getGalleryInfo(galleryID)
return GalleryBlock(
galleryID,
"",
listOf(urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn")),
info.title,
info.artists?.map { it.artist }.orEmpty(),
info.parodys?.map { it.parody }.orEmpty(),
info.type,
info.language.orEmpty(),
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
info.groups?.map { it.group }.orEmpty()
)
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getGalleryInfo
@Serializable
data class GalleryFiles(
val width: Int,
val hash: String,
val haswebp: Int = 0,
val name: String,
val height: Int,
val hasavif: Int = 0,
val hasavifsmalltn: Int? = 0
)
//Set header `Referer` to reader url to avoid 403 error
@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
fun getReader(galleryID: Int) : GalleryInfo {
return getGalleryInfo(galleryID)
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.LinkedList
suspend fun doSearch(query: String, sortMode: SortMode): List<Int> = coroutineScope {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
for (term in terms) {
if (term.startsWith("-"))
negativeTerms.push(term.substring(1))
else if (term.isNotBlank())
positiveTerms.push(term)
}
val positiveResults = positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() }
}
}
val negativeResults = negativeTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, sortMode)
}.getOrElse { emptySet() }
}
}
val results = when {
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(
SearchArgs("all", "index", "all"),
sortMode
)
else -> emptySet()
}.toMutableSet()
fun filterPositive(newResults: Set<Int>) {
when {
results.isEmpty() -> results.addAll(newResults)
else -> results.retainAll(newResults)
}
}
fun filterNegative(newResults: Set<Int>) {
results.removeAll(newResults)
}
//positive results
positiveResults.forEach {
filterPositive(it.await())
}
//negative results
negativeResults.forEach {
filterNegative(it.await())
}
return@coroutineScope if (sortMode != SortMode.RANDOM) {
results.toList()
} else {
results.shuffled()
}
}

View File

@@ -1,409 +0,0 @@
/*
* Copyright 2019 tom5079
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.quaver.pupil.hitomi
import kotlinx.serialization.json.jsonArray
import okhttp3.Request
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.content
import java.net.URL
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import kotlin.math.min
data class SearchArgs(
val area: String?,
val tag: String,
val language: String,
) {
companion object {
fun fromQuery(query: String): SearchArgs? {
if (!query.contains(':')) {
return null
}
val (left, right) = query.split(':')
return when (left) {
"male", "female" -> SearchArgs("tag", query, "all")
"language" -> SearchArgs("all", "index", right)
else -> SearchArgs(left, right, "all")
}
}
}
}
enum class SortMode {
DATE_ADDED,
DATE_PUBLISHED,
POPULAR_TODAY,
POPULAR_WEEK,
POPULAR_MONTH,
POPULAR_YEAR,
RANDOM;
val orderBy: String
get() = when (this) {
DATE_ADDED, DATE_PUBLISHED, RANDOM -> "date"
POPULAR_TODAY, POPULAR_WEEK, POPULAR_MONTH, POPULAR_YEAR -> "popular"
}
val orderByKey: String
get() = when (this) {
DATE_ADDED, RANDOM -> "added"
DATE_PUBLISHED -> "published"
POPULAR_TODAY -> "today"
POPULAR_WEEK -> "week"
POPULAR_MONTH -> "month"
POPULAR_YEAR -> "year"
}
}
//searchlib.js
const val separator = "-"
const val extension = ".html"
const val index_dir = "tagindex"
const val galleries_index_dir = "galleriesindex"
const val max_node_size = 464
const val B = 16
const val compressed_nozomi_prefix = "n"
val tag_index_version: String by lazy { getIndexVersion("tagindex") }
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
val tagIndexDomain = "tagindex.hitomi.la"
fun sha256(data: ByteArray): ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data)
}
@OptIn(ExperimentalUnsignedTypes::class)
fun hashTerm(term: String): UByteArray {
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
}
fun sanitize(input: String): String {
return input.replace(Regex("[/#]"), "")
}
fun getIndexVersion(name: String) =
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
//search.js
fun getGalleryIDsForQuery(query: String, sortMode: SortMode): Set<Int> {
val sanitizedQuery = query.replace("_", " ")
val args = SearchArgs.fromQuery(sanitizedQuery)
return if (args != null) {
getGalleryIDsFromNozomi(args, sortMode)
} else {
val key = hashTerm(sanitizedQuery)
val field = "galleries"
val node = getNodeAtAddress(field, 0)
val data = bSearch(field, key, node)
if (data != null)
return getGalleryIDsFromData(data)
return emptySet()
}
}
fun encodeSearchQueryForUrl(s: Char) =
when (s) {
' ' -> "_"
'/' -> "slash"
'.' -> "dot"
else -> s.toString()
}
fun getSuggestionsForQuery(query: String): List<Suggestion> {
query.replace('_', ' ').let {
var field = "global"
var term = it
if (term.indexOf(':') > -1) {
val sides = it.split(':')
field = sides[0]
term = sides[1]
}
val chars = term.map(::encodeSearchQueryForUrl)
val url =
"https://$tagIndexDomain/$field${if (chars.isNotEmpty()) "/${chars.joinToString("/")}" else ""}.json"
val request = Request.Builder()
.url(url)
.build()
val suggestions = json.parseToJsonElement(
client.newCall(request).execute().body()?.use { body -> body.string() }
?: return emptyList())
return buildList {
suggestions.jsonArray.forEach { suggestionRaw ->
val suggestion = suggestionRaw.jsonArray
if (suggestion.size < 3) {
return@forEach
}
val ns = suggestion[2].content ?: ""
val tagname = sanitize(suggestion[0].content ?: return@forEach)
val url = when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
add(
Suggestion(
suggestion[0].content ?: "",
suggestion[1].content?.toIntOrNull() ?: 0,
url,
ns
)
)
}
}
}
}
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>): List<Suggestion> {
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
val (offset, length) = data
if (length > 10000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset + length))
val suggestions = ArrayList<Suggestion>()
val buffer = ByteBuffer
.wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfSuggestions = buffer.int
if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
throw Exception("number of suggestions $numberOfSuggestions is too long")
for (i in 0.until(numberOfSuggestions)) {
var top = buffer.int
val ns = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
.toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
top = buffer.int
val tag = inbuf.sliceArray(buffer.position().until(buffer.position() + top))
.toString(charset("UTF-8"))
buffer.position(buffer.position() + top)
val count = buffer.int
val tagname = sanitize(tag)
val u =
when (ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
suggestions.add(Suggestion(tag, count, u, ns))
}
return suggestions
}
fun nozomiAddressFromArgs(args: SearchArgs, sortMode: SortMode) = when {
sortMode != SortMode.DATE_ADDED && sortMode != SortMode.RANDOM ->
if (args.area == "all") "$protocol//$domain/$compressed_nozomi_prefix/${sortMode.orderBy}/${sortMode.orderByKey}-${args.language}$nozomiextension"
else "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${sortMode.orderBy}/${sortMode.orderByKey}/${args.tag}-${args.language}$nozomiextension"
args.area == "all" -> "$protocol//$domain/$compressed_nozomi_prefix/${args.tag}-${args.language}$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/${args.area}/${args.tag}-${args.language}$nozomiextension"
}
fun getGalleryIDsFromNozomi(args: SearchArgs, sortMode: SortMode): Set<Int> {
val nozomiAddress = nozomiAddressFromArgs(args, sortMode)
val bytes = URL(nozomiAddress).readBytes()
val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return nozomi
}
fun getGalleryIDsFromData(data: Pair<Long, Int>): Set<Int> {
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
val (offset, length) = data
if (length > 100000000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset + length))
val galleryIDs = mutableSetOf<Int>()
val buffer = ByteBuffer
.wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs * 4 + 4
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
else if (inbuf.size != expectedLength)
throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
for (i in 0.until(numberOfGalleryIDs))
galleryIDs.add(buffer.int)
return galleryIDs
}
fun getNodeAtAddress(field: String, address: Long): Node {
val url =
when (field) {
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
}
val nodedata = getURLAtRange(url, address.until(address + max_node_size))
return decodeNode(nodedata)
}
fun getURLAtRange(url: String, range: LongRange): ByteArray {
val request = Request.Builder()
.url(url)
.header("Range", "bytes=${range.first}-${range.last}")
.build()
return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf()
}
@OptIn(ExperimentalUnsignedTypes::class)
data class Node(
val keys: List<UByteArray>,
val datas: List<Pair<Long, Int>>,
val subNodeAddresses: List<Long>
)
@OptIn(ExperimentalUnsignedTypes::class)
fun decodeNode(data: ByteArray): Node {
val buffer = ByteBuffer
.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
val uData = data.toUByteArray()
val numberOfKeys = buffer.int
val keys = ArrayList<UByteArray>()
for (i in 0.until(numberOfKeys)) {
val keySize = buffer.int
if (keySize == 0 || keySize > 32)
throw Exception("fatal: !keySize || keySize > 32")
keys.add(uData.sliceArray(buffer.position().until(buffer.position() + keySize)))
buffer.position(buffer.position() + keySize)
}
val numberOfDatas = buffer.int
val datas = ArrayList<Pair<Long, Int>>()
for (i in 0.until(numberOfDatas)) {
val offset = buffer.long
val length = buffer.int
datas.add(Pair(offset, length))
}
val numberOfSubNodeAddresses = B + 1
val subNodeAddresses = ArrayList<Long>()
for (i in 0.until(numberOfSubNodeAddresses)) {
val subNodeAddress = buffer.long
subNodeAddresses.add(subNodeAddress)
}
return Node(keys, datas, subNodeAddresses)
}
@OptIn(ExperimentalUnsignedTypes::class)
fun bSearch(field: String, key: UByteArray, node: Node): Pair<Long, Int>? {
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray): Int {
val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) {
if (dv1[i] < dv2[i])
return -1
else if (dv1[i] > dv2[i])
return 1
}
return 0
}
fun locateKey(key: UByteArray, node: Node): Pair<Boolean, Int> {
for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0)
return Pair(cmpResult == 0, i)
}
return Pair(false, node.keys.size)
}
fun isLeaf(node: Node): Boolean {
for (subnode in node.subNodeAddresses)
if (subnode != 0L)
return false
return true
}
if (node.keys.isEmpty())
return null
val (there, where) = locateKey(key, node)
if (there)
return node.datas[where]
else if (isLeaf(node))
return null
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where])
return bSearch(field, key, nextNode)
}

View File

@@ -24,7 +24,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -55,15 +54,13 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
val uri = downloadManager.query(query).use { cursor -> val uri = downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))?.let { cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let {
val uri = Uri.parse(it) val uri = Uri.parse(it)
when (uri.scheme) { when (uri.scheme) {
"file" -> "file" ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!)) )
else
uri
"content" -> uri "content" -> uri
else -> null else -> null
} }
@@ -76,10 +73,10 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), Intent(Intent.ACTION_VIEW).apply { val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")) setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0) }, 0)
val notification = NotificationCompat.Builder(context, "update") val notification = NotificationCompat.Builder(context, "update")
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)

View File

@@ -1,64 +0,0 @@
package xyz.quaver.pupil.receiver
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.wifi.p2p.WifiP2pManager
import android.os.Build
import android.os.Parcelable
import android.util.Log
import androidx.core.app.ActivityCompat
import xyz.quaver.pupil.ui.ErrorType
import xyz.quaver.pupil.ui.TransferStep
import xyz.quaver.pupil.ui.TransferViewModel
private inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? = when {
Build.VERSION.SDK_INT >= 33 -> getParcelableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T
}
class WifiDirectBroadcastReceiver(
private val manager: WifiP2pManager,
private val channel: WifiP2pManager.Channel,
private val viewModel: TransferViewModel
): BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
context!!
when (intent?.action) {
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
Log.d("PUPILD", "Wifi P2P state changed: $state")
viewModel.setWifiP2pEnabled(state == WifiP2pManager.WIFI_P2P_STATE_ENABLED)
}
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
Log.d("PUPILD", "Wifi P2P peers changed")
manager.requestPeers(channel) { peers ->
viewModel.setPeers(peers)
}
}
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
// Respond to new connection or disconnections
val networkInfo = intent.getParcelableExtraCompat<android.net.NetworkInfo>(WifiP2pManager.EXTRA_NETWORK_INFO)
Log.d("PUPILD", "Wifi P2P connection changed: $networkInfo ${networkInfo?.isConnected}")
if (networkInfo?.isConnected == true) {
manager.requestConnectionInfo(channel) { info ->
viewModel.setConnectionInfo(info)
}
} else {
viewModel.setConnectionInfo(null)
}
}
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
// Respond to this device's wifi state changing
Log.d("PUPILD", "Wifi P2P this device changed")
viewModel.setThisDevice(intent.getParcelableExtraCompat(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE))
}
}
}
}

View File

@@ -23,14 +23,10 @@ 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.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -42,9 +38,12 @@ import okhttp3.ResponseBody
import okio.* import okio.*
import xyz.quaver.pupil.* import xyz.quaver.pupil.*
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.cleanCache
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.ellipsize
import xyz.quaver.pupil.util.normalizeID
import xyz.quaver.pupil.util.requestBuilders
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.math.ceil import kotlin.math.ceil
@@ -75,7 +74,7 @@ class DownloadService : Service() {
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0) getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
} }
val action = val action =
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel), NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
@@ -85,7 +84,7 @@ class DownloadService : Service() {
Intent(this, DownloadService::class.java) Intent(this, DownloadService::class.java)
.putExtra(KEY_COMMAND, COMMAND_CANCEL) .putExtra(KEY_COMMAND, COMMAND_CANCEL)
.putExtra(KEY_ID, galleryID), .putExtra(KEY_ID, galleryID),
PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0), PendingIntent.FLAG_UPDATE_CURRENT),
).build() ).build()
notification[galleryID] = NotificationCompat.Builder(this, "download").apply { notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
@@ -101,15 +100,13 @@ class DownloadService : Service() {
notify(galleryID) notify(galleryID)
} }
@SuppressLint("RestrictedApi", "MissingPermission") @SuppressLint("RestrictedApi")
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))
@@ -170,27 +167,15 @@ 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 retry = 5
chain.proceed(request) while (!response.isSuccessful && retry > 0) {
}.getOrNull() response = chain.proceed(request)
var limit = 10 retry--
while (response?.isSuccessful != true) {
if (response?.code() == 503) {
Thread.sleep(200)
} else if (--limit < 0)
break
response = kotlin.runCatching {
chain.proceed(request)
}.getOrNull()
} }
if (response == null) response.newBuilder()
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()
@@ -217,11 +202,14 @@ 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}") e.printStackTrace()
FirebaseCrashlytics.getInstance().recordException(e)
if (e.message?.contains("cancel", true) == false) { if (e.message?.contains("cancel", true) == false) {
val galleryID = (call.request().tag() as Tag).galleryID val galleryID = (call.request().tag() as Tag).galleryID
// Retry
cancel(galleryID)
download(galleryID)
} }
} }
@@ -229,27 +217,29 @@ class DownloadService : Service() {
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()
CoroutineScope(Dispatchers.IO).launch { kotlin.runCatching {
runCatching { val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception()
val image = response.also { if (it.code() != 200) throw IOException( "$galleryID $index ${response.request().url()} CODE ${it.code()}" ) }.body()?.use { it.bytes() } ?: throw Exception("Response null") val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
Cache.getInstance(this@DownloadService, galleryID) CoroutineScope(Dispatchers.IO).launch {
.putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image) kotlin.runCatching {
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
}.onSuccess {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY) if (isCompleted(galleryID)) {
notify(galleryID) if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
if (isCompleted(galleryID)) { startId?.let { stopSelf(it) }
if (DownloadManager.getInstance(this@DownloadService) }
.getDownloadFolder(galleryID) != null }.onFailure {
) it.printStackTrace()
Cache.getInstance(this@DownloadService, galleryID).moveToDownload() cancel(galleryID)
download(galleryID)
startId?.let { stopSelf(it) }
} }
}.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
} }
} }
} }
@@ -315,10 +305,10 @@ class DownloadService : Service() {
initNotification(galleryID) initNotification(galleryID)
val galleryInfo = cache.getGalleryInfo() val reader = cache.getReader()
// Gallery doesn't exist // Gallery doesn't exist
if (galleryInfo == null) { if (reader == null) {
delete(galleryID) delete(galleryID)
progress[galleryID] = mutableListOf() progress[galleryID] = mutableListOf()
return@launch return@launch
@@ -326,7 +316,7 @@ class DownloadService : Service() {
histories.add(galleryID) histories.add(galleryID)
progress[galleryID] = MutableList(galleryInfo.files.size) { 0F } progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
cache.metadata.imageList?.let { cache.metadata.imageList?.let {
it.forEachIndexed { index, image -> it.forEachIndexed { index, image ->
@@ -335,7 +325,8 @@ class DownloadService : Service() {
} }
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService).getDownloadFolder(galleryID) != null) if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload() Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
@@ -343,7 +334,7 @@ class DownloadService : Service() {
return@launch return@launch
} }
notification[galleryID]?.setContentTitle(galleryInfo.title.ellipsize(32)) notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30))
notify(galleryID) notify(galleryID)
val queued = mutableSetOf<Int>() val queued = mutableSetOf<Int>()
@@ -357,7 +348,7 @@ class DownloadService : Service() {
} }
} }
galleryInfo.getRequestBuilders().forEachIndexed { index, it -> reader.requestBuilders.forEachIndexed { index, it ->
if (progress[galleryID]?.get(index)?.isInfinite() == false) { if (progress[galleryID]?.get(index)?.isInfinite() == false) {
val request = it.tag(Tag(galleryID, index, startId)).build() val request = it.tag(Tag(galleryID, index, startId)).build()
client.newCall(request).enqueue(callback) client.newCall(request).enqueue(callback)
@@ -405,11 +396,7 @@ 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_DATA_SYNC)
}
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)
@@ -430,11 +417,7 @@ 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_DATA_SYNC)
}
interceptors[Tag::class] = interceptor interceptors[Tag::class] = interceptor
} }

View File

@@ -1,128 +0,0 @@
package xyz.quaver.pupil.services
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import xyz.quaver.pupil.R
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TransferClientService : Service() {
private val selectorManager = SelectorManager(Dispatchers.IO)
private val channel = Channel<Pair<TransferPacket, Continuation<TransferPacket>>>()
private var job: Job? = null
private fun startForeground() = runCatching {
val notification = NotificationCompat.Builder(this, "transfer")
.setContentTitle("Pupil")
.setContentText("Transfer server is running")
.setSmallIcon(R.drawable.ic_notification)
.build()
ServiceCompat.startForeground(
this,
1,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else 0
)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val address = intent?.getStringExtra("address") ?: run {
stopSelf(startId)
return START_STICKY
}
startForeground()
Log.d("PUPILD", "Starting service with address $address")
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
Log.d("PUPILD", "Connecting to $address")
val socket = aSocket(selectorManager).tcp().connect(address, 12221)
Log.d("PUPILD", "Connected to $address")
val readChannel = socket.openReadChannel()
val writeChannel = socket.openWriteChannel(autoFlush = true)
runCatching {
TransferPacket.Hello().writeToChannel(writeChannel)
val handshake = TransferPacket.readFromChannel(readChannel)
if (handshake !is TransferPacket.Hello || handshake.version != TRANSFER_PROTOCOL_VERSION) {
throw IllegalStateException("Invalid handshake")
}
while (true) {
val (packet, continuation) = channel.receive()
Log.d("PUPILD", "Sending packet $packet")
packet.writeToChannel(writeChannel)
val response = TransferPacket.readFromChannel(readChannel).also {
Log.d("PUPILD", "Received packet $it")
}
continuation.resume(response)
}
}.onFailure {
Log.d("PUPILD", "Connection closed with error $it")
channel.close()
socket.close()
stopSelf(startId)
}
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
job?.cancel()
}
inner class Binder: android.os.Binder() {
@OptIn(DelicateCoroutinesApi::class)
suspend fun sendPacket(packet: TransferPacket): Result<TransferPacket.ListResponse> = runCatching {
check(job != null) { "Service not running" }
check(!channel.isClosedForSend) { "Service not running" }
val response = suspendCoroutine { continuation ->
check (channel.trySend(packet to continuation).isSuccess) { "Service not running" }
}
check (response is TransferPacket.ListResponse) { "Invalid response" }
response
}
}
private val binder = Binder()
override fun onBind(intent: Intent?): IBinder = binder
}

View File

@@ -1,94 +0,0 @@
package xyz.quaver.pupil.services
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
const val TRANSFER_PROTOCOL_VERSION: UByte = 1u
enum class TransferType(val value: UByte) {
INVALID(255u),
HELLO(0u),
PING(1u),
PONG(2u),
LIST_REQUEST(3u),
LIST_RESPONSE(4u),
}
sealed interface TransferPacket {
val type: TransferType
suspend fun writeToChannel(channel: ByteWriteChannel)
data class Hello(val version: UByte = TRANSFER_PROTOCOL_VERSION): TransferPacket {
override val type = TransferType.HELLO
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
channel.writeByte(version.toByte())
}
}
data object Ping: TransferPacket {
override val type = TransferType.PING
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
}
}
data object Pong: TransferPacket {
override val type = TransferType.PONG
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
}
}
data object ListRequest: TransferPacket {
override val type = TransferType.LIST_REQUEST
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
}
}
data object Invalid: TransferPacket {
override val type = TransferType.INVALID
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
}
}
data class ListResponse(
val favoritesCount: Int,
val historyCount: Int,
val downloadsCount: Int,
): TransferPacket {
override val type = TransferType.LIST_RESPONSE
override suspend fun writeToChannel(channel: ByteWriteChannel) {
channel.writeByte(type.value.toByte())
channel.writeInt(favoritesCount)
channel.writeInt(historyCount)
channel.writeInt(downloadsCount)
}
}
companion object {
suspend fun readFromChannel(channel: ByteReadChannel): TransferPacket {
return when(val type = channel.readByte().toUByte()) {
TransferType.HELLO.value -> {
val version = channel.readByte().toUByte()
Hello(version)
}
TransferType.PING.value -> Ping
TransferType.PONG.value -> Pong
TransferType.LIST_REQUEST.value -> ListRequest
TransferType.LIST_RESPONSE.value -> {
val favoritesCount = channel.readInt()
val historyCount = channel.readInt()
val downloadsCount = channel.readInt()
ListResponse(favoritesCount, historyCount, downloadsCount)
}
else -> throw IllegalArgumentException("Unknown packet type: $type")
}
}
}
}

View File

@@ -1,123 +0,0 @@
package xyz.quaver.pupil.services
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.ServerSocket
import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.util.downloader.DownloadManager
class TransferServerService : Service() {
private val selectorManager = SelectorManager(Dispatchers.IO)
private var serverSocket: ServerSocket? = null
private val job = Job()
private fun startForeground() = runCatching {
val notification = NotificationCompat.Builder(this, "transfer")
.setContentTitle("Pupil")
.setContentText("Transfer server is running")
.setSmallIcon(R.drawable.ic_notification)
.build()
ServiceCompat.startForeground(
this,
1,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else 0
)
}
private fun generateListResponse(): TransferPacket.ListResponse {
val favoritesCount = favorites.size
val historyCount = histories.size
val downloadsCount = DownloadManager.getInstance(this).downloadFolderMap.size
return TransferPacket.ListResponse(favoritesCount, historyCount, downloadsCount)
}
private suspend fun handleConnection(socket: Socket) {
val readChannel = socket.openReadChannel()
val writeChannel = socket.openWriteChannel(autoFlush = true)
runCatching {
while (true) {
val packet = TransferPacket.readFromChannel(readChannel)
Log.d("PUPILD", "Received packet $packet")
binder.channel.trySend(packet)
val response = when (packet) {
is TransferPacket.Hello -> TransferPacket.Hello()
is TransferPacket.Ping -> TransferPacket.Pong
is TransferPacket.ListRequest -> generateListResponse()
else -> TransferPacket.Invalid
}
response.writeToChannel(writeChannel)
}
}.onFailure {
socket.close()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val address = intent?.getStringExtra("address") ?: run {
stopSelf(startId)
return START_STICKY
}
if (serverSocket != null) {
return START_STICKY
}
startForeground()
val serverSocket = aSocket(selectorManager).tcp().bind(address, 12221).also {
this@TransferServerService.serverSocket = it
}
CoroutineScope(Dispatchers.IO + job).launch {
while (true) {
Log.d("PUPILD", "Waiting for connection")
val socket = serverSocket.accept()
Log.d("PUPILD", "Accepted connection from ${socket.remoteAddress}")
launch { handleConnection(socket) }
}
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
serverSocket?.close()
}
inner class Binder: android.os.Binder() {
val channel = Channel<TransferPacket>()
}
private val binder = Binder()
override fun onBind(intent: Intent?): IBinder = binder
}

View File

@@ -1,22 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2022 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types
class SendLogException : Exception()
class JavascriptException(message: String?) : Exception(message)

View File

@@ -18,10 +18,10 @@
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.android.parcel.Parcelize
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.hitomi.Suggestion import xyz.quaver.hitomi.Suggestion
import xyz.quaver.pupil.util.translations import xyz.quaver.pupil.util.translations
@Parcelize @Parcelize

View File

@@ -18,67 +18,29 @@
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.normalizeID
open class BaseComponentActivity : ComponentActivity() {
private var locked: Boolean = true
private val lockLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK)
locked = false
else
finish()
}
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
locked = !LockManager(this).locks.isNullOrEmpty()
}
@CallSuper
override fun onResume() {
super.onResume()
if (Preferences["security_mode"])
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked)
lockLauncher.launch(Intent(this, LockActivity::class.java))
}
}
open class BaseActivity : AppCompatActivity() { open class BaseActivity : AppCompatActivity() {
private var locked: Boolean = true private var locked: Boolean = true
private val lockLauncher = private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK)
if (it.resultCode == RESULT_OK) locked = false
locked = false else
else finish()
finish() }
}
@CallSuper @CallSuper
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
@@ -94,12 +56,12 @@ open class BaseActivity : AppCompatActivity() {
if (Preferences["security_mode"]) if (Preferences["security_mode"])
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE)
)
else else
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
if (locked) if (locked)
lockLauncher.launch(Intent(this, LockActivity::class.java)) lockLauncher.launch(Intent(this, LockActivity::class.java))
} }
} }

View File

@@ -29,8 +29,10 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pin_lock.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.LockActivityBinding
import xyz.quaver.pupil.ui.fragment.PINLockFragment import xyz.quaver.pupil.ui.fragment.PINLockFragment
import xyz.quaver.pupil.ui.fragment.PatternLockFragment import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.util.Lock
@@ -43,8 +45,6 @@ class LockActivity : AppCompatActivity() {
private lateinit var lockManager: LockManager private lateinit var lockManager: LockManager
private var mode: String? = null private var mode: String? = null
private lateinit var binding: LockActivityBinding
private val patternLockFragment = PatternLockFragment().apply { private val patternLockFragment = PatternLockFragment().apply {
var lastPass = "" var lastPass = ""
onPatternDrawn = { onPatternDrawn = {
@@ -57,7 +57,7 @@ class LockActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else } else
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG) lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
} }
"add_lock" -> { "add_lock" -> {
if (lastPass.isEmpty()) { if (lastPass.isEmpty()) {
@@ -69,7 +69,7 @@ class LockActivity : AppCompatActivity() {
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it)) LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
finish() finish()
} else { } else {
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG) lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
lastPass = "" lastPass = ""
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show() Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
@@ -92,15 +92,15 @@ class LockActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else { } else {
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply { indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener { setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) { override fun onAnimationEnd(animation: Animation?) {
binding.pinLockView.resetPinLockView() pin_lock_view.resetPinLockView()
binding.pinLockView.isEnabled = true pin_lock_view.isEnabled = true
} }
override fun onAnimationStart(animation: Animation?) { override fun onAnimationStart(animation: Animation?) {
binding.pinLockView.isEnabled = false pin_lock_view.isEnabled = false
} }
override fun onAnimationRepeat(animation: Animation?) { override fun onAnimationRepeat(animation: Animation?) {
@@ -114,22 +114,22 @@ class LockActivity : AppCompatActivity() {
if (lastPass.isEmpty()) { if (lastPass.isEmpty()) {
lastPass = it lastPass = it
binding.pinLockView.resetPinLockView() pin_lock_view.resetPinLockView()
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show() Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
} else { } else {
if (lastPass == it) { if (lastPass == it) {
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it)) LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
finish() finish()
} else { } else {
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply { indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener { setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) { override fun onAnimationEnd(animation: Animation?) {
binding.pinLockView.resetPinLockView() pin_lock_view.resetPinLockView()
binding.pinLockView.isEnabled = true pin_lock_view.isEnabled = true
} }
override fun onAnimationStart(animation: Animation?) { override fun onAnimationStart(animation: Animation?) {
binding.pinLockView.isEnabled = false pin_lock_view.isEnabled = false
} }
override fun onAnimationRepeat(animation: Animation?) { override fun onAnimationRepeat(animation: Animation?) {
@@ -173,8 +173,7 @@ class LockActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = LockActivityBinding.inflate(layoutInflater) setContentView(R.layout.activity_lock)
setContentView(binding.root)
lockManager = try { lockManager = try {
LockManager(this) LockManager(this)
@@ -211,7 +210,7 @@ class LockActivity : AppCompatActivity() {
Preferences["lock_fingerprint"] Preferences["lock_fingerprint"]
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS && BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
) { ) {
binding.fingerprintBtn.apply { lock_fingerprint.apply {
isEnabled = true isEnabled = true
setOnClickListener { setOnClickListener {
showBiometricPrompt() showBiometricPrompt()
@@ -220,7 +219,7 @@ class LockActivity : AppCompatActivity() {
showBiometricPrompt() showBiometricPrompt()
} }
binding.patternBtn.apply { lock_pattern.apply {
isEnabled = lockManager.contains(Lock.Type.PATTERN) isEnabled = lockManager.contains(Lock.Type.PATTERN)
setOnClickListener { setOnClickListener {
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -228,7 +227,7 @@ class LockActivity : AppCompatActivity() {
).commit() ).commit()
} }
} }
binding.pinBtn.apply { lock_pin.apply {
isEnabled = lockManager.contains(Lock.Type.PIN) isEnabled = lockManager.contains(Lock.Type.PIN)
setOnClickListener { setOnClickListener {
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -236,7 +235,7 @@ class LockActivity : AppCompatActivity() {
).commit() ).commit()
} }
} }
binding.passwordBtn.isEnabled = false lock_password.isEnabled = false
when (lockManager.locks!!.first().type) { when (lockManager.locks!!.first().type) {
Lock.Type.PIN -> { Lock.Type.PIN -> {
@@ -254,20 +253,20 @@ class LockActivity : AppCompatActivity() {
} }
} }
"add_lock" -> { "add_lock" -> {
binding.patternBtn.isEnabled = false lock_pattern.isEnabled = false
binding.pinBtn.isEnabled = false lock_pin.isEnabled = false
binding.fingerprintBtn.isEnabled = false lock_fingerprint.isEnabled = false
binding.passwordBtn.isEnabled = false lock_password.isEnabled = false
when(intent.getStringExtra("type")!!) { when(intent.getStringExtra("type")!!) {
"pattern" -> { "pattern" -> {
binding.patternBtn.isEnabled = true lock_pattern.isEnabled = true
supportFragmentManager.beginTransaction().add( supportFragmentManager.beginTransaction().add(
R.id.lock_content, patternLockFragment R.id.lock_content, patternLockFragment
).commit() ).commit()
} }
"pin" -> { "pin" -> {
binding.pinBtn.isEnabled = true lock_pin.isEnabled = true
supportFragmentManager.beginTransaction().add( supportFragmentManager.beginTransaction().add(
R.id.lock_content, pinLockFragment R.id.lock_content, pinLockFragment
).commit() ).commit()

View File

@@ -21,7 +21,6 @@ package xyz.quaver.pupil.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.text.util.Linkify import android.text.util.Linkify
@@ -31,50 +30,29 @@ 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.content.ContextCompat import androidx.appcompat.app.AppCompatDelegate
import androidx.cardview.widget.CardView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CancellationException import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.Deferred import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.SortMode
import xyz.quaver.pupil.hitomi.doSearch
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.searchHistory
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.FavoriteHistorySwitch import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.types.LoadingSuggestion
import xyz.quaver.pupil.types.NoResultSuggestion
import xyz.quaver.pupil.types.Suggestion
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.view.MainView import xyz.quaver.pupil.ui.view.MainView
@@ -84,28 +62,14 @@ import xyz.quaver.pupil.util.Preferences
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.requestNotificationPermission
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.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
val sortModeLookup = mapOf(
R.id.main_menu_sort_date_added to SortMode.DATE_ADDED,
R.id.main_menu_sort_date_published to SortMode.DATE_PUBLISHED,
R.id.main_menu_sort_popular_today to SortMode.POPULAR_TODAY,
R.id.main_menu_sort_popular_week to SortMode.POPULAR_WEEK,
R.id.main_menu_sort_popular_month to SortMode.POPULAR_MONTH,
R.id.main_menu_sort_popular_year to SortMode.POPULAR_YEAR,
R.id.main_menu_sort_random to SortMode.RANDOM
)
class MainActivity : class MainActivity :
BaseActivity(), BaseActivity(),
NavigationView.OnNavigationItemSelectedListener { NavigationView.OnNavigationItemSelectedListener
{
enum class Mode { enum class Mode {
SEARCH, SEARCH,
@@ -114,102 +78,60 @@ class MainActivity :
FAVORITE FAVORITE
} }
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Int>() private val galleries = ArrayList<Int>()
private var query = "" private var query = ""
set(value) { set(value) {
field = value field = value
with(findViewById<SearchInputView>(R.id.search_bar_text)) { with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value) if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE) setText(query, TextView.BufferType.EDITABLE)
}
} }
}
private var queryStack = mutableListOf<String>() private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH private var mode = Mode.SEARCH
private var sortMode = SortMode.DATE_ADDED private var sortMode = SortMode.NEWEST
private var galleryIDs: Deferred<List<Int>>? = null private var galleryIDs: Deferred<List<Int>>? = null
private var totalItems = 0 private var totalItems = 0
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var currentPage = 0 private var currentPage = 0
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?) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater) setContentView(R.layout.activity_main)
setContentView(binding.root)
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url -> intent.dataString?.let { url ->
restore(url, restore(url,
onFailure = { onFailure = {
Snackbar.make( Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
binding.contents.recyclerview,
R.string.settings_backup_failed,
Snackbar.LENGTH_LONG
).show()
}, onSuccess = { }, onSuccess = {
Snackbar.make( Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show()
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( DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
supportFragmentManager,
"Download Location Dialog"
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] && checkUpdate(this)
ContextCompat.getExternalFilesDirs(this, null).filterNotNull()
.map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""])
) {
AlertDialog.Builder(this)
.setTitle(R.string.warning)
.setMessage(R.string.unaccessible_download_folder)
.setPositiveButton(android.R.string.ok) { _, _ ->
DownloadLocationDialogFragment().show(
supportFragmentManager,
"Download Location Dialog"
)
}.setNegativeButton(R.string.ignore) { _, _ ->
Preferences["download_folder_ignore_warning"] = true
}.show()
}
initView() initView()
} }
override fun onResume() { @OptIn(ExperimentalStdlibApi::class)
super.onResume()
checkUpdate(this)
}
override fun onBackPressed() { override fun onBackPressed() {
when { when {
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer( main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START)
GravityCompat.START
)
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last() query = queryStack.last()
@@ -218,7 +140,6 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
} }
} }
@@ -226,14 +147,14 @@ class MainActivity :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
(binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false (main_recyclerview?.adapter as? GalleryBlockAdapter)?.updateAll = false
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when (keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) { if (currentPage > 0) {
runOnUiThread { runOnUiThread {
@@ -248,7 +169,6 @@ class MainActivity :
true true
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) { if (currentPage < maxPage) {
runOnUiThread { runOnUiThread {
@@ -263,48 +183,41 @@ class MainActivity :
true true
} }
else -> super.onKeyDown(keyCode, event) else -> super.onKeyDown(keyCode, event)
} }
} }
private fun initView() { private fun initView() {
binding.contents.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { main_recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// -height of the search view < translationY < 0 // -height of the search view < translationY < 0
binding.contents.searchview.translationY = main_searchview.translationY =
min( min(
max( max(
binding.contents.searchview.translationY - dy, main_searchview.translationY - dy,
-binding.contents.searchview.binding.querySection.root.height.toFloat() -main_searchview.findViewById<CardView>(R.id.search_query_section).height.toFloat()
), 0F ), 0F)
)
if (dy > 0) if (dy > 0)
binding.contents.fab.hideMenuButton(true) main_fab.hideMenuButton(true)
else if (dy < 0) else if (dy < 0)
binding.contents.fab.showMenuButton(true) main_fab.showMenuButton(true)
} }
}) })
Linkify.addLinks( Linkify.addLinks(main_noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
binding.contents.noresult,
Pattern.compile(getString(R.string.https_text)),
null,
null,
{ _, _ -> getString(R.string.https) })
//NavigationView //NavigationView
binding.navView.setNavigationItemSelectedListener(this) main_nav_view.setNavigationItemSelectedListener(this)
with(binding.contents.cancelFab) { with(main_fab_cancel) {
setImageResource(R.drawable.cancel) setImageResource(R.drawable.cancel)
setOnClickListener { setOnClickListener {
DownloadService.cancel(this@MainActivity) DownloadService.cancel(this@MainActivity)
} }
} }
with(binding.contents.jumpFab) { with(main_fab_jump) {
setImageResource(R.drawable.ic_jump) setImageResource(R.drawable.ic_jump)
setOnClickListener { setOnClickListener {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
@@ -313,17 +226,14 @@ class MainActivity :
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setView(editText) setView(editText)
setTitle(R.string.main_jump_title) setTitle(R.string.main_jump_title)
setMessage( setMessage(getString(
getString( R.string.main_jump_message,
R.string.main_jump_message, currentPage+1,
currentPage + 1, ceil(totalItems / perPage.toDouble()).roundToInt()
ceil(totalItems / perPage.toDouble()).roundToInt() ))
)
)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
(editText.text.toString().toIntOrNull() ?: return@setPositiveButton) - 1
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
@@ -335,7 +245,7 @@ class MainActivity :
} }
} }
with(binding.contents.randomFab) { with(main_fab_random) {
setImageResource(R.drawable.shuffle_variant) setImageResource(R.drawable.shuffle_variant)
setOnClickListener { setOnClickListener {
runBlocking { runBlocking {
@@ -365,7 +275,7 @@ class MainActivity :
} }
} }
with(binding.contents.idFab) { with(main_fab_id) {
setImageResource(R.drawable.numeric) setImageResource(R.drawable.numeric)
setOnClickListener { setOnClickListener {
val editText = EditText(context).apply { val editText = EditText(context).apply {
@@ -377,8 +287,7 @@ class MainActivity :
setTitle(R.string.main_open_gallery_by_id) setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton
editText.text.toString().toIntOrNull() ?: return@setPositiveButton
GalleryDialog(this@MainActivity, galleryID).apply { GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
@@ -399,15 +308,15 @@ class MainActivity :
} }
} }
with(binding.contents.view) { with(main_view) {
setOnPageTurnListener(object : MainView.OnPageTurnListener { setOnPageTurnListener(object: MainView.OnPageTurnListener {
override fun onPrev(page: Int) { override fun onPrev(page: Int) {
currentPage-- currentPage--
// disable pageturn until the contents are loaded // disable pageturn until the contents are loaded
setCurrentPage(1, false) setCurrentPage(1, false)
ViewCompat.animate(binding.contents.searchview) ViewCompat.animate(main_searchview)
.setDuration(100) .setDuration(100)
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.translationY(0F) .translationY(0F)
@@ -424,7 +333,7 @@ class MainActivity :
// disable pageturn until the contents are loaded // disable pageturn until the contents are loaded
setCurrentPage(1, false) setCurrentPage(1, false)
ViewCompat.animate(binding.contents.searchview) ViewCompat.animate(main_searchview)
.setDuration(100) .setDuration(100)
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.translationY(0F) .translationY(0F)
@@ -445,7 +354,7 @@ class MainActivity :
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(binding.contents.recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply { adapter = GalleryBlockAdapter(galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
@@ -461,18 +370,12 @@ class MainActivity :
onDownloadClickedHandler = { position -> onDownloadClickedHandler = { position ->
val galleryID = galleries[position] val galleryID = galleries[position]
requestNotificationPermission( if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress
this@MainActivity, DownloadService.cancel(this@MainActivity, galleryID)
requestNotificationPermssionLauncher }
) { else {
if (DownloadManager.getInstance(context) DownloadManager.getInstance(context).addDownloadFolder(galleryID)
.isDownloading(galleryID) DownloadService.download(this@MainActivity, galleryID)
) { //download in progress
DownloadService.cancel(this@MainActivity, galleryID)
} else {
DownloadManager.getInstance(context).addDownloadFolder(galleryID)
DownloadService.download(this@MainActivity, galleryID)
}
} }
closeAllItems() closeAllItems()
@@ -542,7 +445,6 @@ class MainActivity :
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
} + FavoriteHistorySwitch(getString(R.string.search_show_histories)) } + FavoriteHistorySwitch(getString(R.string.search_show_histories))
} }
else -> { else -> {
searchHistory.map { searchHistory.map {
Suggestion(it) Suggestion(it)
@@ -550,29 +452,12 @@ class MainActivity :
} }
}.reversed() }.reversed()
private var suggestionJob: Job? = null private var suggestionJob : Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
with(binding.contents.searchview) { with(main_searchview as xyz.quaver.pupil.ui.view.FloatingSearchView) {
val scrollSuggestionToTop = { onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
with(binding.suggestionSection.suggestionsList) {
MainScope().launch {
withTimeout(1000) {
val layoutManager = layoutManager as LinearLayoutManager
while (layoutManager.findLastVisibleItemPosition() != adapter?.itemCount?.minus(
1
)
) {
layoutManager.scrollToPosition(adapter?.itemCount?.minus(1) ?: 0)
delay(100)
}
}
}
}
}
onMenuStatusChangeListener = object : FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
} }
override fun onMenuClosed() { override fun onMenuClosed() {
@@ -595,7 +480,6 @@ class MainActivity :
onFavoriteHistorySwitchClickListener = { onFavoriteHistorySwitchClickListener = {
isFavorite = !isFavorite isFavorite = !isFavorite
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
scrollSuggestionToTop()
} }
onMenuItemClickListener = { onMenuItemClickListener = {
@@ -609,7 +493,6 @@ class MainActivity :
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
scrollSuggestionToTop()
return@lambda return@lambda
} }
@@ -622,8 +505,7 @@ class MainActivity :
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = kotlin.runCatching { val suggestions = kotlin.runCatching {
getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) } getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList()
.toMutableList()
}.getOrElse { mutableListOf() } }.getOrElse { mutableListOf() }
suggestions.filter { suggestions.filter {
@@ -635,21 +517,15 @@ class MainActivity :
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
swapSuggestions( swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
if (suggestions.isNotEmpty()) suggestions else listOf(
NoResultSuggestion(getText(R.string.main_no_result).toString())
)
)
} }
} }
} }
onFocusChangeListener = object : FloatingSearchView.OnFocusChangeListener { onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (query.isEmpty() or query.endsWith(' ')) { if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(defaultSuggestions) swapSuggestions(defaultSuggestions)
scrollSuggestionToTop()
}
} }
override fun onFocusCleared() { override fun onFocusCleared() {
@@ -665,24 +541,18 @@ class MainActivity :
} }
} }
attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer) attachNavigationDrawerToMenuButton(main_drawer_layout)
} }
} }
fun onActionMenuItemSelected(item: MenuItem?) { fun onActionMenuItemSelected(item: MenuItem?) {
when (item?.itemId) { when(item?.itemId) {
R.id.main_menu_settings -> startActivity( R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
Intent(
this@MainActivity,
SettingsActivity::class.java
)
)
R.id.main_menu_thin -> { R.id.main_menu_thin -> {
val thin = !item.isChecked val thin = !item.isChecked
item.isChecked = thin item.isChecked = thin
binding.contents.recyclerview.apply { main_recyclerview.apply {
(adapter as GalleryBlockAdapter).apply { (adapter as GalleryBlockAdapter).apply {
this.thin = thin this.thin = thin
@@ -692,15 +562,21 @@ class MainActivity :
adapter = adapter // Force to redraw adapter = adapter // Force to redraw
} }
} }
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
item.isChecked = true
R.id.main_menu_sort_date_added, runOnUiThread {
R.id.main_menu_sort_date_published, currentPage = 0
R.id.main_menu_sort_popular_today,
R.id.main_menu_sort_popular_week, cancelFetch()
R.id.main_menu_sort_popular_month, clearGalleries()
R.id.main_menu_sort_popular_year, fetchGalleries(query, sortMode)
R.id.main_menu_sort_random -> { loadBlocks()
sortMode = sortModeLookup[item.itemId]!! }
}
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
item.isChecked = true item.isChecked = true
runOnUiThread { runOnUiThread {
@@ -717,9 +593,9 @@ class MainActivity :
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
runOnUiThread { runOnUiThread {
binding.drawer.closeDrawers() main_drawer_layout.closeDrawers()
when (item.itemId) { when(item.itemId) {
R.id.main_drawer_home -> { R.id.main_drawer_home -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -730,7 +606,6 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_history -> { R.id.main_drawer_history -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -741,7 +616,6 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_downloads -> { R.id.main_drawer_downloads -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -752,7 +626,6 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_favorite -> { R.id.main_drawer_favorite -> {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -763,35 +636,20 @@ class MainActivity :
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
} }
R.id.main_drawer_github -> { R.id.main_drawer_github -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github))))
} }
R.id.main_drawer_homepage -> { R.id.main_drawer_homepage -> {
startActivity( startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page))))
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.home_page))
)
)
} }
R.id.main_drawer_email -> { R.id.main_drawer_email -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
} }
R.id.main_drawer_kakaotalk -> { R.id.main_drawer_kakaotalk -> {
startActivity( startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.discord))
)
)
} }
} }
} }
@@ -807,14 +665,14 @@ class MainActivity :
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch { private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
galleries.clear() galleries.clear()
with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) { with(main_recyclerview.adapter as GalleryBlockAdapter?) {
this ?: return@with this ?: return@with
this.notifyDataSetChanged() this.notifyDataSetChanged()
} }
binding.contents.noresult.visibility = View.INVISIBLE main_noresult.visibility = View.INVISIBLE
binding.contents.progressbar.show() main_progressbar.show()
} }
private fun fetchGalleries(query: String, sortMode: SortMode) { private fun fetchGalleries(query: String, sortMode: SortMode) {
@@ -829,18 +687,17 @@ class MainActivity :
} }
if (query.isNotEmpty() && mode != Mode.SEARCH) { if (query.isNotEmpty() && mode != Mode.SEARCH) {
Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT) Snackbar.make(this@MainActivity.main_recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
.apply { setAction(android.R.string.ok) {
setAction(android.R.string.ok) { cancelFetch()
cancelFetch() clearGalleries()
clearGalleries() currentPage = 0
currentPage = 0 mode = Mode.SEARCH
mode = Mode.SEARCH queryStack.clear()
queryStack.clear() fetchGalleries(query, sortMode)
fetchGalleries(query, sortMode) loadBlocks()
loadBlocks() }
} }.show()
}.show()
} }
galleryIDs = null galleryIDs = null
@@ -849,16 +706,22 @@ class MainActivity :
return return
galleryIDs = CoroutineScope(Dispatchers.IO).async { galleryIDs = CoroutineScope(Dispatchers.IO).async {
when (mode) { when(mode) {
Mode.SEARCH -> { Mode.SEARCH -> {
doSearch( when {
"$defaultQuery $query", query.isEmpty() and defaultQuery.isEmpty() -> {
sortMode when(sortMode) {
).also { SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
totalItems = it.size else -> getGalleryIDsFromNozomi(null, "index", "all")
}.also {
totalItems = it.size
}
}
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also {
totalItems = it.size
}
} }
} }
Mode.HISTORY -> { Mode.HISTORY -> {
when { when {
query.isEmpty() -> { query.isEmpty() -> {
@@ -866,42 +729,36 @@ class MainActivity :
totalItems = it.size totalItems = it.size
} }
} }
else -> { else -> {
val result = doSearch(query, SortMode.DATE_ADDED).sorted() val result = doSearch(query).sorted()
histories.reversed().filter { result.binarySearch(it) >= 0 }.also { histories.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList()
when { when {
query.isEmpty() -> downloads.reversed().also { query.isEmpty() -> downloads.reversed().also {
totalItems = it.size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query, SortMode.DATE_ADDED).sorted() val result = doSearch(query).sorted()
downloads.reversed().filter { result.binarySearch(it) >= 0 }.also { downloads.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
} }
} }
} }
Mode.FAVORITE -> { Mode.FAVORITE -> {
when { when {
query.isEmpty() -> favorites.reversed().also { query.isEmpty() -> favorites.reversed().also {
totalItems = it.size totalItems = it.size
} }
else -> { else -> {
val result = doSearch(query, SortMode.DATE_ADDED).sorted() val result = doSearch(query).sorted()
favorites.reversed().filter { result.binarySearch(it) >= 0 }.also { favorites.reversed().filter { result.binarySearch(it) >= 0 }.also {
totalItems = it.size totalItems = it.size
} }
@@ -922,30 +779,23 @@ class MainActivity :
throw Exception("No result") throw Exception("No result")
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e !is CancellationException)
if (e.message != "No result")
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.contents.noresult.visibility = View.VISIBLE main_noresult.visibility = View.VISIBLE
binding.contents.progressbar.hide() main_progressbar.hide()
} }
return@launch return@launch
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
binding.contents.view.setCurrentPage( main_view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
currentPage + 1,
galleryIDs.size > (currentPage + 1) * perPage
)
} }
galleryIDs.slice( galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
currentPage * perPage until min(
currentPage * perPage + perPage,
galleryIDs.size
)
).chunked(5).let { chunks ->
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {
@@ -956,10 +806,10 @@ class MainActivity :
}.forEach { }.forEach {
it.await()?.also { it.await()?.also {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.contents.progressbar.hide() main_progressbar.hide()
galleries.add(it) galleries.add(it)
binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1) main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
} }
} }
} }

View File

@@ -1,44 +0,0 @@
package xyz.quaver.pupil.ui
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.adaptive.calculateDisplayFeatures
import xyz.quaver.pupil.ui.compose.MainApp
import xyz.quaver.pupil.ui.theme.AppTheme
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.requestNotificationPermission
import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
class MainComposeActivity : BaseComponentActivity() {
private val requestNotificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
showNotificationPermissionExplanationDialog(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestNotificationPermission(this, requestNotificationPermissionLauncher, false)
val viewModel: MainViewModel by viewModels()
setContent {
val displayFeatures = calculateDisplayFeatures(this)
val uiState by viewModel.searchState.collectAsStateWithLifecycle()
AppTheme {
MainApp(
uiState = uiState,
displayFeatures = displayFeatures
)
}
}
}
}

View File

@@ -45,24 +45,24 @@ import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.android.synthetic.main.reader_eye_card.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites 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() {
@@ -75,7 +75,7 @@ class ReaderActivity : BaseActivity() {
set(value) { set(value) {
field = value field = value
(binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value (reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value
} }
private lateinit var cache: Cache private lateinit var cache: Cache
@@ -118,18 +118,9 @@ class ReaderActivity : BaseActivity() {
private var eyeType: Eye? = null private var eyeType: Eye? = null
private var eyeTime: Long = 0L private var eyeTime: Long = 0L
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) setContentView(R.layout.activity_reader)
setContentView(binding.root)
title = getString(R.string.reader_loading) title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
@@ -157,11 +148,10 @@ 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 = if (uri.host?.endsWith("hasha.in") == true) { galleryID = when (uri.host) {
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
} }
@@ -171,10 +161,12 @@ class ReaderActivity : BaseActivity() {
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reader, menu) menuInflater.inflate(R.menu.reader, menu)
with(menu.findItem(R.id.reader_menu_favorite)) { with(menu?.findItem(R.id.reader_menu_favorite)) {
this ?: return@with
if (favorites.contains(galleryID)) if (favorites.contains(galleryID))
(icon as Animatable).start() (icon as Animatable).start()
} }
@@ -186,19 +178,17 @@ class ReaderActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) { when(item.itemId) {
R.id.reader_menu_page_indicator -> { R.id.reader_menu_page_indicator -> {
// TODO: Switch to DialogFragment val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false)
val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false) with(view.dialog_number_picker) {
with(binding.numberPicker) {
minValue = 1 minValue = 1
maxValue = cache.metadata.galleryInfo?.files?.size ?: 0 maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0
value = currentPage value = currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
setView(binding.root) setView(view)
}.create() }.create()
binding.okButton.setOnClickListener { view.dialog_ok.setOnClickListener {
(this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0) (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.value-1, 0)
dialog.dismiss() dialog.dismiss()
} }
@@ -268,12 +258,12 @@ class ReaderActivity : BaseActivity() {
//currentPage is 1-based //currentPage is 1-based
return when(keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0) (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
true true
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0) (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
true true
} }
@@ -295,35 +285,42 @@ class ReaderActivity : BaseActivity() {
if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
update = false update = false
Snackbar Snackbar
.make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE) .make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show() .show()
return@launch return@launch
} }
binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0 reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
binding.downloadProgressbar.progress = reader_download_progressbar.progress =
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0 downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val galleryInfo = cache.metadata.galleryInfo val reader = cache.metadata.reader
if (galleryInfo != null) { if (reader != null) {
with(binding.recyclerview.adapter as ReaderAdapter) { with(reader_recyclerview.adapter as ReaderAdapter) {
this.galleryInfo = galleryInfo this.reader = reader
notifyDataSetChanged() notifyDataSetChanged()
} }
title = galleryInfo.title title = reader.galleryInfo.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = menu?.findItem(R.id.reader_menu_page_indicator)?.title =
"$currentPage/${galleryInfo.files.size}" "$currentPage/${reader.galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi) menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(
this@ReaderActivity,
when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
}
)
} }
} }
if (downloader.isCompleted(galleryID)) { //Download finished if (downloader.isCompleted(galleryID)) { //Download finished
binding.downloadProgressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false) animateDownloadFAB(false)
} }
@@ -332,7 +329,7 @@ class ReaderActivity : BaseActivity() {
} }
private fun initView() { private fun initView() {
with(binding.recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply { adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
@@ -342,7 +339,7 @@ class ReaderActivity : BaseActivity() {
scrollMode(false) scrollMode(false)
fullscreen(true) fullscreen(true)
} else { } else {
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
} }
} }
} }
@@ -352,9 +349,9 @@ class ReaderActivity : BaseActivity() {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (dy < 0) if (dy < 0)
binding.fab.showMenuButton(true) this@ReaderActivity.reader_fab.showMenuButton(true)
else if (dy > 0) else if (dy > 0)
binding.fab.hideMenuButton(true) this@ReaderActivity.reader_fab.hideMenuButton(true)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager val layoutManager = recyclerView.layoutManager as LinearLayoutManager
@@ -366,36 +363,31 @@ class ReaderActivity : BaseActivity() {
}) })
} }
with(binding.downloadFab) { with(reader_fab_download) {
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( val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
this@ReaderActivity,
requestNotificationPermssionLauncher
) {
val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
if (downloadManager.isDownloading(galleryID)) { if (downloadManager.isDownloading(galleryID)) {
downloadManager.deleteDownloadFolder(galleryID) downloadManager.deleteDownloadFolder(galleryID)
animateDownloadFAB(false) animateDownloadFAB(false)
} else { } else {
downloadManager.addDownloadFolder(galleryID) downloadManager.addDownloadFolder(galleryID)
DownloadService.download(context, galleryID, true) DownloadService.download(context, galleryID, true)
animateDownloadFAB(true) animateDownloadFAB(true)
}
} }
} }
} }
with(binding.retryFab) { with(reader_fab_retry) {
setImageResource(R.drawable.refresh) setImageResource(R.drawable.refresh)
setOnClickListener { setOnClickListener {
DownloadService.download(context, galleryID) DownloadService.download(context, galleryID)
} }
} }
with(binding.autoFab) { with(reader_fab_auto) {
setImageResource(R.drawable.eye_white) setImageResource(R.drawable.eye_white)
setOnClickListener { setOnClickListener {
when { when {
@@ -415,13 +407,13 @@ class ReaderActivity : BaseActivity() {
} }
} }
with(binding.fullscreenFab) { with(reader_fab_fullscreen) {
setImageResource(R.drawable.ic_fullscreen) setImageResource(R.drawable.ic_fullscreen)
setOnClickListener { setOnClickListener {
isFullscreen = true isFullscreen = true
fullscreen(isFullscreen) fullscreen(isFullscreen)
binding.fab.close(true) this@ReaderActivity.reader_fab.close(true)
} }
} }
} }
@@ -431,8 +423,8 @@ class ReaderActivity : BaseActivity() {
if (isFullscreen) { if (isFullscreen) {
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
supportActionBar?.hide() supportActionBar?.hide()
binding.fab.visibility = View.INVISIBLE this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
binding.scroller.let { this@ReaderActivity.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height) it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width) it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal) it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
@@ -441,8 +433,8 @@ class ReaderActivity : BaseActivity() {
} else { } else {
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv() flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
supportActionBar?.show() supportActionBar?.show()
binding.fab.visibility = View.VISIBLE this@ReaderActivity.reader_fab.visibility = View.VISIBLE
binding.scroller.let { this@ReaderActivity.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width) it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height) it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb) it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
@@ -453,27 +445,27 @@ class ReaderActivity : BaseActivity() {
window.attributes = this window.attributes = this
} }
binding.recyclerview.adapter = binding.recyclerview.adapter // Force to redraw reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw
} }
private fun scrollMode(isScroll: Boolean) { private fun scrollMode(isScroll: Boolean) {
if (isScroll) { if (isScroll) {
snapHelper.attachToRecyclerView(null) snapHelper.attachToRecyclerView(null)
binding.recyclerview.layoutManager = LinearLayoutManager(this) reader_recyclerview.layoutManager = LinearLayoutManager(this)
} else { } else {
snapHelper.attachToRecyclerView(binding.recyclerview) snapHelper.attachToRecyclerView(reader_recyclerview)
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) { reader_recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) { override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
extraLayoutSpace.fill(600) extraLayoutSpace.fill(600)
} }
} }
} }
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0) (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
} }
private fun animateDownloadFAB(animate: Boolean) { private fun animateDownloadFAB(animate: Boolean) {
with(binding.downloadFab) { with(reader_fab_download) {
if (animate) { if (animate) {
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading) val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
@@ -502,7 +494,7 @@ class ReaderActivity : BaseActivity() {
} }
val cameraCallback: (List<Face>) -> Unit = callback@{ faces -> val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
binding.eyeCard.dot.let { eye_card.dot.let {
it.visibility = View.VISIBLE it.visibility = View.VISIBLE
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
delay(50) delay(50)
@@ -512,9 +504,9 @@ class ReaderActivity : BaseActivity() {
if (faces.size != 1) if (faces.size != 1)
ContextCompat.getDrawable(this, R.drawable.eye_off).let { ContextCompat.getDrawable(this, R.drawable.eye_off).let {
with(binding.eyeCard) { with(eye_card) {
leftEye.setImageDrawable(it) left_eye.setImageDrawable(it)
rightEye.setImageDrawable(it) right_eye.setImageDrawable(it)
} }
return@callback return@callback
@@ -525,16 +517,16 @@ class ReaderActivity : BaseActivity() {
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
) )
with(binding.eyeCard) { with(eye_card) {
leftEye.setImageDrawable( left_eye.setImageDrawable(
ContextCompat.getDrawable( ContextCompat.getDrawable(
leftEye.context, context,
if (left) R.drawable.eye else R.drawable.eye_closed if (left) R.drawable.eye else R.drawable.eye_closed
) )
) )
rightEye.setImageDrawable( right_eye.setImageDrawable(
ContextCompat.getDrawable( ContextCompat.getDrawable(
rightEye.context, context,
if (right) R.drawable.eye else R.drawable.eye_closed if (right) R.drawable.eye else R.drawable.eye_closed
) )
) )
@@ -561,7 +553,7 @@ class ReaderActivity : BaseActivity() {
} }
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) { if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
(binding.recyclerview.layoutManager as LinearLayoutManager).let { (this@ReaderActivity.reader_recyclerview.layoutManager as LinearLayoutManager).let {
it.scrollToPositionWithOffset(when(eyeType!!) { it.scrollToPositionWithOffset(when(eyeType!!) {
Eye.RIGHT -> { Eye.RIGHT -> {
if (it.reverseLayout) currentPage - 2 else currentPage if (it.reverseLayout) currentPage - 2 else currentPage
@@ -577,11 +569,11 @@ class ReaderActivity : BaseActivity() {
} }
private fun toggleCamera() { private fun toggleCamera() {
val eyes = binding.eyeCard.root val eyes = this@ReaderActivity.eye_card
when (camera) { when (camera) {
null -> { null -> {
binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel) reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel)
binding.autoFab.setImageResource(R.drawable.eye_off_white) reader_fab_auto.setImageResource(R.drawable.eye_off_white)
eyes.apply { eyes.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
TranslateAnimation(0F, 0F, -100F, 0F).apply { TranslateAnimation(0F, 0F, -100F, 0F).apply {
@@ -594,8 +586,8 @@ class ReaderActivity : BaseActivity() {
cameraEnabled = true cameraEnabled = true
} }
else -> { else -> {
binding.autoFab.labelText = getString(R.string.reader_fab_auto) reader_fab_auto.labelText = getString(R.string.reader_fab_auto)
binding.autoFab.setImageResource(R.drawable.eye_white) reader_fab_auto.setImageResource(R.drawable.eye_white)
eyes.apply { eyes.apply {
TranslateAnimation(0F, 0F, 0F, -100F).apply { TranslateAnimation(0F, 0F, 0F, -100F).apply {
duration = 500 duration = 500

View File

@@ -1,382 +0,0 @@
package xyz.quaver.pupil.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.wifi.WpsInfo
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver
import xyz.quaver.pupil.services.TransferClientService
import xyz.quaver.pupil.services.TransferPacket
import xyz.quaver.pupil.services.TransferServerService
import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment
import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment
import xyz.quaver.pupil.ui.fragment.TransferPermissionFragment
import xyz.quaver.pupil.ui.fragment.TransferSelectDataFragment
import xyz.quaver.pupil.ui.fragment.TransferTargetFragment
import xyz.quaver.pupil.ui.fragment.TransferWaitForConnectionFragment
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
private val viewModel: TransferViewModel by viewModels()
private val intentFilter = IntentFilter().apply {
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
}
private lateinit var manager: WifiP2pManager
private lateinit var channel: WifiP2pManager.Channel
private var receiver: BroadcastReceiver? = null
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
viewModel.setStep(TransferStep.TARGET)
} else {
viewModel.setStep(TransferStep.PERMISSION)
}
}
private var clientServiceBinder: TransferClientService.Binder? = null
private val clientServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
clientServiceBinder = service as TransferClientService.Binder
}
override fun onServiceDisconnected(name: ComponentName?) {
clientServiceBinder = null
}
}
private fun checkPermission(force: Boolean = false): Boolean {
val permissionRequired = if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) {
Manifest.permission.ACCESS_FINE_LOCATION
} else {
Manifest.permission.NEARBY_WIFI_DEVICES
}
val permissionGranted =
ActivityCompat.checkSelfPermission(this, permissionRequired) == PackageManager.PERMISSION_GRANTED
val shouldShowRationale =
ActivityCompat.shouldShowRequestPermissionRationale(this, permissionRequired)
if (!permissionGranted) {
if (shouldShowRationale && force) {
viewModel.setStep(TransferStep.PERMISSION)
} else {
requestPermissionLauncher.launch(permissionRequired)
}
return false
}
return true
}
private fun handleServerResponse(response: TransferPacket?) {
when (response) {
is TransferPacket.ListResponse -> {
Log.d("PUPILD", "Received list response $response")
}
else -> {
Log.d("PUPILD", "Received invalid response $response")
}
}
}
private suspend fun WifiP2pManager.disconnect() {
suspendCoroutine { continuation ->
removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
continuation.resume(Unit)
}
override fun onFailure(reason: Int) {
continuation.resume(Unit)
}
})
}
suspendCoroutine { continuation ->
cancelConnect(channel, object: WifiP2pManager.ActionListener {
override fun onSuccess() {
continuation.resume(Unit)
}
override fun onFailure(reason: Int) {
continuation.resume(Unit)
}
})
}
}
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.hide()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
manager = getSystemService(WIFI_P2P_SERVICE) as WifiP2pManager
channel = manager.initialize(this, mainLooper, null)
viewModel.peerToConnect.observe(this) { peer ->
if (peer == null) { return@observe }
if (!checkPermission()) { return@observe }
val config = WifiP2pConfig().apply {
deviceAddress = peer.deviceAddress
wps.setup = WpsInfo.PBC
}
manager.connect(channel, config, object: WifiP2pManager.ActionListener {
override fun onSuccess() { }
override fun onFailure(reason: Int) {
viewModel.connect(null)
}
})
}
lifecycleScope.launch {
viewModel.messageQueue.consumeEach {
clientServiceBinder?.sendPacket(it)?.getOrNull()?.let(::handleServerResponse)
}
}
lifecycleScope.launch {
viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest step@{ step ->
when (step) {
TransferStep.TARGET,
TransferStep.TARGET_FORCE -> {
if (!checkPermission(step == TransferStep.TARGET_FORCE)) {
return@step
}
manager.discoverPeers(channel, object: WifiP2pManager.ActionListener {
override fun onSuccess() { }
override fun onFailure(reason: Int) {
}
})
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferTargetFragment())
}
val hostAddress = viewModel.connectionInfo.filterNotNull().first {
it.groupFormed
}.groupOwnerAddress.hostAddress
val intent = Intent(this@TransferActivity, TransferClientService::class.java).also {
it.putExtra("address", hostAddress)
}
ContextCompat.startForegroundService(this@TransferActivity, intent)
bindService(intent, clientServiceConnection, BIND_AUTO_CREATE)
viewModel.setStep(TransferStep.SELECT_DATA)
}
TransferStep.DIRECTION -> {
manager.disconnect()
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferDirectionFragment())
}
}
TransferStep.PERMISSION -> {
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferPermissionFragment())
}
}
TransferStep.WAIT_FOR_CONNECTION -> {
Log.d("PUPILD", "wait for connection")
if (!checkPermission()) { return@step }
runCatching {
suspendCoroutine { continuation ->
manager.createGroup(channel, object: WifiP2pManager.ActionListener {
override fun onSuccess() {
continuation.resume(Unit)
}
override fun onFailure(reason: Int) {
continuation.resumeWithException(Exception("Failed to create group $reason"))
}
})
}
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
}
val address = viewModel.connectionInfo.filterNotNull().first {
it.groupFormed && it.isGroupOwner
}.groupOwnerAddress.hostAddress
val intent = Intent(this@TransferActivity, TransferServerService::class.java).also {
it.putExtra("address", address)
}
ContextCompat.startForegroundService(this@TransferActivity, intent)
val binder: TransferServerService.Binder = suspendCoroutine { continuation ->
bindService(intent, object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
continuation.resume(service as TransferServerService.Binder)
}
override fun onServiceDisconnected(name: ComponentName?) { }
}, BIND_AUTO_CREATE)
}
binder.channel.receive()
viewModel.setStep(TransferStep.CONNECTED)
}.onFailure {
Log.e("PUPILD", "Failed to create group", it)
}
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
}
}
TransferStep.CONNECTED -> {
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferConnectedFragment())
}
}
TransferStep.SELECT_DATA -> {
supportFragmentManager.commit(true) {
replace(R.id.fragment_container_view, TransferSelectDataFragment())
}
}
}
}
}
}
override fun onResume() {
super.onResume()
bindService(Intent(this, TransferClientService::class.java), clientServiceConnection, BIND_AUTO_CREATE)
WifiDirectBroadcastReceiver(manager, channel, viewModel).also {
receiver = it
registerReceiver(it, intentFilter)
}
}
override fun onPause() {
super.onPause()
unbindService(clientServiceConnection)
receiver?.let { unregisterReceiver(it) }
receiver = null
}
}
enum class TransferStep {
TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION, CONNECTED, SELECT_DATA
}
enum class ErrorType {
}
class TransferViewModel : ViewModel() {
private val _step: MutableStateFlow<TransferStep> = MutableStateFlow(TransferStep.DIRECTION)
val step: StateFlow<TransferStep> = _step
private val _error = MutableLiveData<ErrorType?>(null)
val error: LiveData<ErrorType?> = _error
private val _wifiP2pEnabled: MutableLiveData<Boolean> = MutableLiveData(false)
val wifiP2pEnabled: LiveData<Boolean> = _wifiP2pEnabled
private val _thisDevice: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
val thisDevice: LiveData<WifiP2pDevice?> = _thisDevice
private val _peers: MutableLiveData<WifiP2pDeviceList?> = MutableLiveData(null)
val peers: LiveData<WifiP2pDeviceList?> = _peers
private val _connectionInfo: MutableStateFlow<WifiP2pInfo?> = MutableStateFlow(null)
val connectionInfo: StateFlow<WifiP2pInfo?> = _connectionInfo
private val _peerToConnect: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
val peerToConnect: LiveData<WifiP2pDevice?> = _peerToConnect
val messageQueue: Channel<TransferPacket> = Channel()
fun setStep(step: TransferStep) {
Log.d("PUPILD", "Set step: $step")
_step.value = step
}
fun setWifiP2pEnabled(enabled: Boolean) {
_wifiP2pEnabled.value = enabled
}
fun setThisDevice(device: WifiP2pDevice?) {
_thisDevice.value = device
}
fun setPeers(peers: WifiP2pDeviceList?) {
_peers.value = peers
}
fun setConnectionInfo(info: WifiP2pInfo?) {
_connectionInfo.value = info
}
fun setError(error: ErrorType?) {
_error.value = error
}
fun connect(device: WifiP2pDevice?) {
_peerToConnect.value = device
}
fun ping() {
messageQueue.trySend(TransferPacket.Ping)
}
fun list() {
messageQueue.trySend(TransferPacket.ListRequest)
}
}

View File

@@ -1,20 +0,0 @@
package xyz.quaver.pupil.ui.compose
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import androidx.window.core.layout.WindowSizeClass
import androidx.window.layout.DisplayFeature
import xyz.quaver.pupil.ui.viewmodel.SearchState
@Composable
fun MainApp(
uiState: SearchState,
displayFeatures: List<DisplayFeature>,
navController: NavController = rememberNavController(),
windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass,
) {
Text("Hello, World!")
}

View File

@@ -18,30 +18,30 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.dialog_default_query.*
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
class DefaultQueryDialog : DialogFragment() { class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val languages: Map<String, String> by lazy { private val languages = context.resources.getStringArray(R.array.languages).map {
requireContext().resources.getStringArray(R.array.languages).map { it.split("|").let { split ->
it.split("|").let { split -> Pair(split[0], split[1])
Pair(split[0], split[1]) }
} }.toMap()
}.toMap() private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
}
private val reverseLanguages: Map<String, String> by lazy {
languages.entries.associate { (k, v) -> v to k }
}
private val excludeBL = "-male:yaoi" private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro") private val excludeGuro = listOf("-female:guro", "-male:guro")
@@ -49,15 +49,46 @@ class DefaultQueryDialog : DialogFragment() {
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
private var _binding: DefaultQueryDialogBinding? = null @SuppressLint("InflateParams")
private val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.default_query_dialog_title)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
private fun initView() { with(default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
if (default_query_dialog_loli_checkbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
super.onCreate(savedInstanceState)
}
@SuppressLint("InflateParams")
private fun build() : View {
val tags = Tags.parse( val tags = Tags.parse(
Preferences["default_query"] Preferences["default_query"]
) )
with(binding.languageSelector) { val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
with(view.default_query_dialog_language_selector) {
adapter = adapter =
ArrayAdapter( ArrayAdapter(
context, context,
@@ -80,13 +111,13 @@ class DefaultQueryDialog : DialogFragment() {
} }
} }
with(binding.BLCheckbox) { with(view.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL) isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL)) if (tags.contains(excludeBL))
tags.remove(excludeBL) tags.remove(excludeBL)
} }
with(binding.guroCheckbox) { with(view.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) } isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) }) if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach { excludeGuro.forEach {
@@ -94,7 +125,7 @@ class DefaultQueryDialog : DialogFragment() {
} }
} }
with(binding.loliCheckbox) { with(view.default_query_dialog_loli_checkbox) {
isChecked = excludeLoli.all { tags.contains(it) } isChecked = excludeLoli.all { tags.contains(it) }
if (excludeLoli.all { tags.contains(it) }) if (excludeLoli.all { tags.contains(it) })
excludeLoli.forEach { excludeLoli.forEach {
@@ -102,7 +133,7 @@ class DefaultQueryDialog : DialogFragment() {
} }
} }
with(binding.edittext) { with(view.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged( override fun beforeTextChanged(
@@ -122,50 +153,13 @@ class DefaultQueryDialog : DialogFragment() {
s.replace( s.replace(
0, 0,
s.length, s.length,
s.toString().lowercase() s.toString().toLowerCase(java.util.Locale.getDefault())
) )
} }
}) })
} }
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return view
_binding = DefaultQueryDialogBinding.inflate(layoutInflater)
initView()
return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.default_query_dialog_title)
setView(binding.root)
setPositiveButton(android.R.string.ok) { _, _ ->
val newTags = Tags.parse(binding.edittext.text.toString())
with(binding.languageSelector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (binding.BLCheckbox.isChecked)
newTags.add(excludeBL)
if (binding.guroCheckbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
if (binding.loliCheckbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
}.create()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
} }
} }

View File

@@ -18,17 +18,17 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.formatDownloadFolder import xyz.quaver.pupil.util.formatDownloadFolder
@@ -37,49 +37,38 @@ import xyz.quaver.pupil.util.formatMap
class DownloadFolderNameDialogFragment : DialogFragment() { class DownloadFolderNameDialogFragment : DialogFragment() {
private var _binding: DownloadFolderNameDialogBinding? = null @SuppressLint("InflateParams")
private val binding get() = _binding!! private fun build(): View {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
initView()
return Dialog(requireContext()).apply {
setContentView(binding.root)
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun initView() {
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) } val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
CoroutineScope(Dispatchers.IO).launch { val galleryBlock = runBlocking {
val galleryBlock = Cache.getInstance(requireContext(), galleryID).getGalleryBlock() Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
binding.edittext.addTextChangedListener {
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
}
} }
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"]) return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply {
binding.okButton.setOnClickListener { message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
val newValue = binding.edittext.text.toString() edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
edittext.addTextChangedListener {
if ((newValue as? String)?.contains("/") != false) { message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
Snackbar.make(binding.root, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
} }
ok_button.setOnClickListener {
val newValue = edittext.text.toString()
Preferences["download_folder_name"] = binding.edittext.text.toString() if ((newValue as? String)?.contains("/") != false) {
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
}
dismiss() Preferences["download_folder_name"] = edittext.text.toString()
dismiss()
}
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
Dialog(requireContext()).apply {
setContentView(build())
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
}
} }

View File

@@ -18,45 +18,52 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts 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.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
import kotlinx.android.synthetic.main.item_download_folder.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.toFile import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.migrate
import java.io.File import java.io.File
class DownloadLocationDialogFragment : DialogFragment() { class DownloadLocationDialogFragment : DialogFragment() {
private val entries = mutableMapOf<File?, View>()
private var _binding: DownloadLocationDialogBinding? = null
private val binding get() = _binding!!
private val entries = mutableMapOf<File?, DownloadLocationItemBinding>()
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
val activity = activity ?: return@registerForActivityResult
val context = context ?: return@registerForActivityResult val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult val dialog = dialog ?: return@registerForActivityResult
it.data?.data?.also { uri -> it.data?.data?.also { uri ->
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION val takeFlags: Int =
activity.intent.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
context.contentResolver.takePersistableUriPermission(uri, takeFlags) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) { if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath entries[null]?.location_available?.text = uri.toFile(context)?.canonicalPath
Preferences["download_folder"] = uri.toString() Preferences["download_folder"] = uri.toString()
} else { } else {
Snackbar.make( Snackbar.make(
@@ -68,89 +75,122 @@ class DownloadLocationDialogFragment : DialogFragment() {
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder } val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder if (key == null) entries[key]!!.location_available.text = downloadFolder
} }
} }
} else {
val downloadFolder = DownloadManager.getInstance(context ?: return@registerForActivityResult).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
if (key == null)
entries[key]!!.locationAvailable.text = downloadFolder
else {
entries[null]!!.button.isChecked = false
entries[key]!!.button.isChecked = true
}
} }
} }
private fun initView() { private val requestDownloadFolderOldLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null) val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult
if (it.resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = it.data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite()) {
Snackbar.make(
dialog.window!!.decorView.rootView,
R.string.settings_download_folder_not_writable,
Snackbar.LENGTH_LONG
).show()
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder
}
else {
entries[null]?.location_available?.text = directory
Preferences["download_folder"] = File(directory).toURI().toString()
}
}
}
@SuppressLint("InflateParams")
private fun build() : View? {
val context = context ?: return null
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
externalFilesDirs.forEachIndexed { index, dir -> externalFilesDirs.forEachIndexed { index, dir ->
dir ?: return@forEachIndexed dir ?: return@forEachIndexed
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply { view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
locationType.text = requireContext().getString(when (index) { location_type.text = context.getString(when (index) {
0 -> R.string.settings_download_folder_internal 0 -> R.string.settings_download_folder_internal
else -> R.string.settings_download_folder_removable else -> R.string.settings_download_folder_removable
}) })
locationAvailable.text = requireContext().getString( location_available.text = context.getString(
R.string.settings_download_folder_available, R.string.settings_download_folder_available,
byteToString(dir.freeSpace) byteToString(dir.freeSpace)
) )
root.setOnClickListener { setOnClickListener {
entries.values.forEach { entry -> entries.values.forEach {
entry.button.isChecked = false it.button.isChecked = false
} }
button.performClick() button.performClick()
Preferences["download_folder"] = dir.toUri().toString() Preferences["download_folder"] = dir.toUri().toString()
} }
entries[dir] = this entries[dir] = this
} })
} }
DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply { view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply {
locationType.text = requireContext().getString(R.string.settings_download_folder_custom) location_type.text = context.getString(R.string.settings_download_folder_custom)
root.setOnClickListener { setOnClickListener {
entries.values.forEach { entry -> entries.values.forEach {
entry.button.isChecked = false it.button.isChecked = false
} }
button.performClick() button.performClick()
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
putExtra("android.content.extra.SHOW_ADVANCED", true) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
} putExtra("android.content.extra.SHOW_ADVANCED", true)
}
requestDownloadFolderLauncher.launch(intent) requestDownloadFolderLauncher.launch(intent)
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil")
.allowNewDirectoryNameModification(true)
.build()
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
}
requestDownloadFolderOldLauncher.launch(intent)
}
} }
entries[null] = this entries[null] = this
} })
val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder } val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.locationAvailable.text = downloadFolder if (key == null) entries[key]!!.location_available.text = downloadFolder
return view
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DownloadLocationDialogBinding.inflate(layoutInflater) val builder = AlertDialog.Builder(requireContext())
initView() builder
.setTitle(R.string.settings_download_folder)
return AlertDialog.Builder(requireContext()).apply { .setView(build())
setTitle(R.string.settings_download_folder) .setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
setView(binding.root)
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: "" Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
DownloadManager.getInstance(requireContext()).migrate()
} }
isCancelable = false isCancelable = false
}.create()
}
override fun onDestroy() { return builder.create()
super.onDestroy()
_binding = null
} }
} }

View File

@@ -22,6 +22,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
@@ -31,16 +32,19 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
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.coroutines.withContext import kotlinx.coroutines.withContext
import xyz.quaver.pupil.hitomi.Gallery import xyz.quaver.hitomi.Gallery
import xyz.quaver.pupil.hitomi.getGallery import xyz.quaver.hitomi.getGallery
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.databinding.*
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
@@ -55,12 +59,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
private lateinit var binding: GalleryDialogBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = GalleryDialogBinding.inflate(layoutInflater) setContentView(R.layout.dialog_gallery)
setContentView(binding.root)
window?.attributes.apply { window?.attributes.apply {
this ?: return@apply this ?: return@apply
@@ -69,7 +70,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
height = LayoutParams.MATCH_PARENT height = LayoutParams.MATCH_PARENT
} }
with(binding.fab) { with(gallery_fab) {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right)) setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
setOnClickListener { setOnClickListener {
context.startActivity(Intent(context, ReaderActivity::class.java).apply { context.startActivity(Intent(context, ReaderActivity::class.java).apply {
@@ -82,12 +83,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
try { try {
val gallery = getGallery(galleryID) val gallery = getGallery(galleryID)
launch (Dispatchers.Main) { gallery_cover.post {
binding.progressbar.visibility = View.GONE gallery_progressbar.visibility = View.GONE
binding.title.text = gallery.title gallery_title.text = gallery.title
binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
with(binding.type) { with(gallery_type) {
text = gallery.type.wordCapitalize() text = gallery.type.wordCapitalize()
setOnClickListener { setOnClickListener {
gallery.type.let { gallery.type.let {
@@ -104,14 +105,14 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
} }
binding.cover.showImage(Uri.parse(gallery.cover)) gallery_cover.showImage(Uri.parse(gallery.cover))
addDetails(gallery) addDetails(gallery)
addThumbnails(gallery) addThumbnails(gallery)
addRelated(gallery) addRelated(gallery)
} }
} catch (e: Exception) { } catch (e: Exception) {
Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply { Snackbar.make(gallery_layout, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply {
if (Locale.getDefault().language == "ko") if (Locale.getDefault().language == "ko")
setAction(context.getText(R.string.https_text)) { setAction(context.getText(R.string.https_text)) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https)))) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https))))
@@ -122,8 +123,10 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
private fun addDetails(gallery: Gallery) { private fun addDetails(gallery: Gallery) {
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { val inflater = LayoutInflater.from(context)
type.setText(R.string.gallery_details)
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details)
listOf( listOf(
R.string.gallery_artists, R.string.gallery_artists,
@@ -160,13 +163,13 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
) )
).filter { ).filter {
(_, content) -> content.isNotEmpty() (_, content) -> content.isNotEmpty()
}.forEach { (title, content) -> }.forEach { (title, content) ->
GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply { inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
type.setText(title) gallery_details_type.setText(title)
content.forEach { tag -> content.forEach { tag ->
tags.addView( gallery_details_tags.addView(
TagChip(context, tag).apply { TagChip(context, tag).apply {
setOnClickListener { setOnClickListener {
onChipClickedHandler.forEach { handler -> onChipClickedHandler.forEach { handler ->
@@ -176,34 +179,42 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
) )
} }
}.let {
gallery_details_contents.addView(it)
} }
} }
}.let {
gallery_contents.addView(it)
} }
} }
private fun addThumbnails(gallery: Gallery) { private fun addThumbnails(gallery: Gallery) {
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { val inflater = LayoutInflater.from(context)
type.setText(R.string.gallery_thumbnails)
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails)
val pager = ViewPager2(context).apply { val pager = ViewPager2(context).apply {
adapter = ThumbnailPageAdapter(gallery.thumbnails) adapter = ThumbnailPageAdapter(gallery.thumbnails)
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
} }
contents.addView( gallery_details_contents.addView(
pager, pager,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
) )
// TODO: Change to direct allocation LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents)
GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply {
dotindicator.setViewPager2(pager) gallery_dotindicator.setViewPager2(pager)
} }.let {
gallery_contents.addView(it)
} }
} }
private fun addRelated(gallery: Gallery) { private fun addRelated(gallery: Gallery) {
val galleries = mutableListOf<Int>() val inflater = LayoutInflater.from(context)
val galleries = ArrayList<Int>()
val adapter = GalleryBlockAdapter(galleries).apply { val adapter = GalleryBlockAdapter(galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
@@ -213,10 +224,10 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
} }
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
type.setText(R.string.gallery_related) gallery_details.setText(R.string.gallery_related)
contents.addView(RecyclerView(context).apply { RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
this.adapter = adapter this.adapter = adapter
@@ -236,18 +247,22 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
true true
} }
} }
}) }.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID -> gallery.related.forEach { galleryID ->
Cache.getInstance(context, galleryID).getGalleryBlock()?.let { Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
galleries.add(galleryID) galleries.add(galleryID)
}
} }
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
}
} }
} }
} }

View File

@@ -0,0 +1,91 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.MirrorAdapter
import xyz.quaver.pupil.util.Preferences
class MirrorDialog(context: Context) : AlertDialog(context) {
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
var onMoveItem : ((Int, Int) -> (Unit))? = null
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_mirror_title)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun build() : View {
return RecyclerView(context).apply recyclerview@{
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context)
adapter = MirrorAdapter(context).apply adapter@{
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
onMoveItem = this@adapter.onItemMove
}).apply {
attachToRecyclerView(this@recyclerview)
}
onStartDrag = {
itemTouchHelper.startDrag(it)
}
onItemMoved = {
Preferences["mirrors"] = it.joinToString(">")
}
}
}
}
}

View File

@@ -18,60 +18,58 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.dialog_proxy.view.*
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.databinding.ProxyDialogBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.ProxyInfo import xyz.quaver.pupil.util.ProxyInfo
import xyz.quaver.pupil.util.getProxyInfo import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.proxyInfo import xyz.quaver.pupil.util.proxyInfo
import java.net.Proxy import java.net.Proxy
class ProxyDialogFragment : DialogFragment() { class ProxyDialog(context: Context) : AlertDialog(context) {
private var _binding: ProxyDialogBinding? = null override fun onCreate(savedInstanceState: Bundle?) {
private val binding get() = _binding!! setView(build())
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { super.onCreate(savedInstanceState)
_binding = ProxyDialogBinding.inflate(layoutInflater)
initView()
return AlertDialog.Builder(requireContext()).apply {
setView(binding.root)
}.create()
} }
private fun initView() { @SuppressLint("InflateParams")
private fun build() : View {
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
val enabler = { enable: Boolean -> val enabler = { enable: Boolean ->
binding.addr.isEnabled = enable view?.proxy_addr?.isEnabled = enable
binding.port.isEnabled = enable view?.proxy_port?.isEnabled = enable
binding.username.isEnabled = enable view?.proxy_username?.isEnabled = enable
binding.password.isEnabled = enable view?.proxy_password?.isEnabled = enable
if (!enable) { if (!enable) {
binding.addr.text = null view?.proxy_addr?.text = null
binding.port.text = null view?.proxy_port?.text = null
binding.username.text = null view?.proxy_username?.text = null
binding.password.text = null view?.proxy_password?.text = null
} }
} }
with(binding.typeSelector) { with(view.proxy_type_selector) {
adapter = ArrayAdapter( adapter = ArrayAdapter(
context, context,
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
@@ -89,29 +87,29 @@ class ProxyDialogFragment : DialogFragment() {
} }
} }
binding.addr.setText(proxyInfo.host) view.proxy_addr.setText(proxyInfo.host)
binding.port.setText(proxyInfo.port?.toString()) view.proxy_port.setText(proxyInfo.port?.toString())
binding.username.setText(proxyInfo.username) view.proxy_username.setText(proxyInfo.username)
binding.password.setText(proxyInfo.password) view.proxy_password.setText(proxyInfo.password)
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT) enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
binding.cancelButton.setOnClickListener { view.proxy_cancel.setOnClickListener {
dismiss() dismiss()
} }
binding.okButton.setOnClickListener { view.proxy_ok.setOnClickListener {
val type = Proxy.Type.values()[binding.typeSelector.selectedItemPosition] val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition]
val addr = binding.addr.text?.toString() val addr = view.proxy_addr.text?.toString()
val port = binding.port.text?.toString()?.toIntOrNull() val port = view.proxy_port.text?.toString()?.toIntOrNull()
val username = binding.username.text?.toString() val username = view.proxy_username.text?.toString()
val password = binding.password.text?.toString() val password = view.proxy_password.text?.toString()
if (type != Proxy.Type.DIRECT) { if (type != Proxy.Type.DIRECT) {
if (addr == null || addr.isEmpty()) if (addr == null || addr.isEmpty())
binding.addr.error = requireContext().getText(R.string.proxy_dialog_error) view.proxy_addr.error = context.getText(R.string.proxy_dialog_error)
if (port == null) if (port == null)
binding.port.error = requireContext().getText(R.string.proxy_dialog_error) view.proxy_port.error = context.getText(R.string.proxy_dialog_error)
if (addr == null || addr.isEmpty() || port == null) if (addr == null || addr.isEmpty() || port == null)
return@setOnClickListener return@setOnClickListener
@@ -128,6 +126,8 @@ class ProxyDialogFragment : DialogFragment() {
dismiss() dismiss()
} }
return view
} }
} }

View File

@@ -18,72 +18,25 @@
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)
@@ -94,44 +47,57 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
val context = context ?: return val context = context ?: return
findPreference<Preference>("backup")?.setOnPreferenceClickListener { findPreference<Preference>("backup")?.setOnPreferenceClickListener {
val favorites = runCatching { val request = Request.Builder()
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText()) .url(context.getString(R.string.backup_url))
}.getOrNull() .post(
val favoriteTags = kotlin.runCatching { FormBody.Builder()
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText()) .add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText())
}.getOrNull() .build()
).build()
val favoriteJson = buildJsonObject { client.newCall(request).enqueue(object: Callback {
favorites?.let { override fun onFailure(call: Call, e: IOException) {
put("favorites", it) val view = view ?: return
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) {
if (response.code() != 200) {
response.close()
return
}
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, response.body()?.use { it.string() }?.replace("\n", ""))
}.let {
getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
}
} }
} })
val backupFile = File(context.filesDir, "pupil-backup.json").also {
it.writeText(favoriteJson.toString())
}
Intent(Intent.ACTION_SEND).apply {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", backupFile)
setDataAndType(uri, "application/json")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, uri)
}.let {
context.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share)))
}
true true
} }
findPreference<Preference>("restore")?.setOnPreferenceClickListener { findPreference<Preference>("restore")?.setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { val editText = EditText(context).apply {
addCategory(Intent.CATEGORY_OPENABLE) setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE)
type = "*/*"
} }
requestBackupFileLauncher.launch(intent) AlertDialog.Builder(context)
.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

@@ -18,38 +18,22 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.SAFileX
import xyz.quaver.io.util.deleteRecursively import xyz.quaver.io.util.deleteRecursively
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.json
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
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.downloader.Metadata
import java.io.File import java.io.File
import kotlin.math.roundToInt
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
@@ -61,10 +45,12 @@ 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")
@@ -94,48 +80,6 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
setNegativeButton(android.R.string.cancel) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show() }.show()
} }
"recover_downloads" -> {
val density = context.resources.displayMetrics.density
this.icon = object: CircularProgressDrawable(context) {
override fun getIntrinsicHeight() = (24*density).roundToInt()
override fun getIntrinsicWidth() = (24*density).roundToInt()
}.apply {
setStyle(CircularProgressDrawable.DEFAULT)
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
start()
}
val downloadManager = DownloadManager.getInstance(context)
val downloadFolderMap = downloadManager.downloadFolderMap
downloadFolderMap.clear()
downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder ->
val metadataFile = FileX(context, folder, ".metadata")
if (!metadataFile.exists()) return@forEach
val metadata = metadataFile.readText()?.let {
runCatching {
json.decodeFromString<Metadata>(it)
}.getOrNull()
} ?: return@forEach
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
downloadFolderMap[galleryID] = folder.name
}
downloadManager.downloadFolderMap.putAll(downloadFolderMap)
val downloads = FileX(context, downloadManager.downloadFolder, ".download")
if (!downloads.exists()) downloads.createNewFile()
downloads.writeText(Json.encodeToString(downloadFolderMap))
this.icon = null
Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show()
}
"delete_downloads" -> { "delete_downloads" -> {
val dir = DownloadManager.getInstance(context).downloadFolder val dir = DownloadManager.getInstance(context).downloadFolder
@@ -150,12 +94,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
} }
if (dir.exists()) if (dir.exists())
dir.listFiles()?.forEach { dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() }
when (it) {
is FileX -> it.deleteRecursively()
else -> it.deleteRecursively()
}
}
job = launch { job = launch {
var size = 0L var size = 0L
@@ -247,12 +186,6 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
onPreferenceClickListener = this@ManageStorageFragment onPreferenceClickListener = this@ManageStorageFragment
} }
with(findPreference<Preference>("recover_downloads")) {
this ?: return@with
onPreferenceClickListener = this@ManageStorageFragment
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -24,34 +24,30 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.andrognito.pinlockview.PinLockListener import com.andrognito.pinlockview.PinLockListener
import xyz.quaver.pupil.databinding.PinLockFragmentBinding import kotlinx.android.synthetic.main.fragment_pin_lock.view.*
import xyz.quaver.pupil.R
class PINLockFragment : Fragment() { class PINLockFragment : Fragment(), PinLockListener {
private var _binding: PinLockFragmentBinding? = null
val binding get() = _binding!!
var onPINEntered: ((String) -> Unit)? = null var onPINEntered: ((String) -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = PinLockFragmentBinding.inflate(inflater, container, false) return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply {
pin_lock_view.attachIndicatorDots(indicator_dots)
binding.pinLockView.attachIndicatorDots(binding.indicatorDots) pin_lock_view.setPinLockListener(this@PINLockFragment)
binding.pinLockView.setPinLockListener(object: PinLockListener { }
override fun onComplete(p0: String?) {
onPINEntered?.invoke(p0 ?: "")
}
override fun onEmpty() {}
override fun onPinChange(p0: Int, p1: String?) {}
})
return binding.root
} }
override fun onDestroy() { override fun onComplete(pin: String?) {
super.onDestroy() onPINEntered?.invoke(pin!!)
_binding = null }
override fun onEmpty() {
}
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
} }
} }

View File

@@ -26,36 +26,38 @@ import androidx.fragment.app.Fragment
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
import com.andrognito.patternlockview.listener.PatternLockViewListener import com.andrognito.patternlockview.listener.PatternLockViewListener
import com.andrognito.patternlockview.utils.PatternLockUtils import com.andrognito.patternlockview.utils.PatternLockUtils
import xyz.quaver.pupil.databinding.PatternLockFragmentBinding import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
import xyz.quaver.pupil.R
class PatternLockFragment : Fragment() { class PatternLockFragment : Fragment(), PatternLockViewListener {
private var _binding: PatternLockFragmentBinding? = null
val binding get() = _binding!!
var onPatternDrawn: ((String) -> Unit)? = null var onPatternDrawn: ((String) -> Unit)? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View? {
_binding = PatternLockFragmentBinding.inflate(inflater, container, false) return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply {
binding.patternLockView.addPatternLockListener(object: PatternLockViewListener { lock_pattern_view.addPatternLockListener(this@PatternLockFragment)
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) { }
val password = PatternLockUtils.patternToMD5(binding.patternLockView, pattern)
onPatternDrawn?.invoke(password)
}
override fun onCleared() {}
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {}
override fun onStarted() {}
})
return binding.root
} }
override fun onDestroy() { override fun onCleared() {
super.onDestroy()
_binding = null }
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
val password = PatternLockUtils.patternToMD5(lock_pattern_view, pattern)
onPatternDrawn?.invoke(password)
}
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {
}
override fun onStarted() {
} }
} }

View File

@@ -21,33 +21,23 @@ package xyz.quaver.pupil.ui.fragment
import android.app.Activity import android.app.Activity
import android.content.* import android.content.*
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.* import androidx.preference.*
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport
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 okhttp3.Dispatcher
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.types.SendLogException
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.TransferActivity
import xyz.quaver.pupil.ui.dialog.* import xyz.quaver.pupil.ui.dialog.*
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import java.util.* import java.util.*
import java.util.concurrent.Executors
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@@ -83,8 +73,10 @@ 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)
@@ -93,12 +85,12 @@ class SettingsFragment :
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog().apply { DefaultQueryDialog(requireContext()).apply {
onPositiveButtonClickListener = { newTags -> onPositiveButtonClickListener = { newTags ->
Preferences["default_query"] = newTags.toString() Preferences["default_query"] = newTags.toString()
summary = newTags.toString() summary = newTags.toString()
} }
}.show(parentFragmentManager, "Default Query Dialog") }.show()
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(requireContext(), LockActivity::class.java).apply { val intent = Intent(requireContext(), LockActivity::class.java).apply {
@@ -106,19 +98,20 @@ class SettingsFragment :
} }
lockLauncher.launch(intent) lockLauncher.launch(intent)
} }
"mirrors" -> {
MirrorDialog(requireContext())
.show()
}
"proxy" -> { "proxy" -> {
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog") ProxyDialog(requireContext())
.show()
} }
"user_id" -> { "user_id" -> {
FirebaseCrashlytics.getInstance().recordException(SendLogException())
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("user_id", Preferences.get<String>("user_id")) ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
) )
Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
} }
"transfer_data" -> {
activity?.startActivity(Intent(activity, TransferActivity::class.java))
}
else -> return false else -> return false
} }
} }
@@ -126,8 +119,10 @@ 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()
@@ -165,7 +160,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
@@ -173,18 +168,6 @@ class SettingsFragment :
"download_folder_name" -> { "download_folder_name" -> {
summary = Preferences["download_folder_name", "[-id-] -title-"] summary = Preferences["download_folder_name", "[-id-] -title-"]
} }
"max_concurrent_download" -> {
val newValue = Preferences.get<String>(key).toIntOrNull() ?: 0
if (newValue == 0)
clientBuilder.dispatcher(Dispatcher())
else
clientBuilder.dispatcher((Dispatcher(Executors.newFixedThreadPool(newValue))))
clientHolder = null
client
}
else -> return
} }
} }
} }
@@ -264,11 +247,6 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"proxy" -> {
summary = getProxyInfo().type.name
onPreferenceClickListener = this@SettingsFragment
}
"tag_translation" -> { "tag_translation" -> {
this as ListPreference this as ListPreference
@@ -290,6 +268,14 @@ class SettingsFragment :
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"proxy" -> {
summary = getProxyInfo().type.name
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }
@@ -302,13 +288,10 @@ class SettingsFragment :
} }
"oss" -> { "oss" -> {
setOnPreferenceClickListener { setOnPreferenceClickListener {
context.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
true true
} }
} }
"transfer_data" -> {
onPreferenceClickListener = this@SettingsFragment
}
} }
} }

View File

@@ -1,10 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import androidx.fragment.app.Fragment
import xyz.quaver.pupil.R
class TransferConnectedFragment: Fragment(R.layout.transfer_connected_fragment) {
}

View File

@@ -1,44 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.TransferDirectionFragmentBinding
import xyz.quaver.pupil.ui.TransferStep
import xyz.quaver.pupil.ui.TransferViewModel
class TransferDirectionFragment : Fragment(R.layout.transfer_direction_fragment) {
private var _binding: TransferDirectionFragmentBinding? = null
private val binding get() = _binding!!
private val viewModel: TransferViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = TransferDirectionFragmentBinding.inflate(inflater, container, false)
binding.inButton.setOnClickListener {
viewModel.setStep(TransferStep.TARGET)
}
binding.outButton.setOnClickListener {
viewModel.setStep(TransferStep.WAIT_FOR_CONNECTION)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,38 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import xyz.quaver.pupil.databinding.TransferPermissionFragmentBinding
import xyz.quaver.pupil.ui.TransferStep
import xyz.quaver.pupil.ui.TransferViewModel
class TransferPermissionFragment: Fragment() {
private var _binding: TransferPermissionFragmentBinding? = null
private val binding get() = _binding!!
private val viewModel: TransferViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = TransferPermissionFragmentBinding.inflate(inflater, container, false)
binding.permissionsButton.setOnClickListener {
viewModel.setStep(TransferStep.TARGET_FORCE)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,37 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import xyz.quaver.pupil.databinding.TransferSelectDataFragmentBinding
import xyz.quaver.pupil.ui.TransferViewModel
class TransferSelectDataFragment: Fragment() {
private var _binding: TransferSelectDataFragmentBinding? = null
private val binding get() = _binding!!
private val viewModel: TransferViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false)
binding.checkAll.setOnCheckedChangeListener { _, isChecked ->
viewModel.list()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,69 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.TransferPeersAdapter
import xyz.quaver.pupil.databinding.TransferTargetFragmentBinding
import xyz.quaver.pupil.ui.TransferStep
import xyz.quaver.pupil.ui.TransferViewModel
class TransferTargetFragment : Fragment() {
private var _binding: TransferTargetFragmentBinding? = null
private val binding get() = _binding!!
private val viewModel: TransferViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = TransferTargetFragmentBinding.inflate(inflater, container, false)
viewModel.thisDevice.observe(viewLifecycleOwner) { device ->
if (device == null) {
return@observe
}
if (device.status == 3) {
binding.ripple.startRippleAnimation()
binding.retryButton.visibility = View.INVISIBLE
} else {
binding.ripple.stopRippleAnimation()
binding.retryButton.visibility = View.VISIBLE
}
}
viewModel.peers.observe(viewLifecycleOwner) { peers ->
if (peers == null) {
return@observe
}
binding.deviceList.adapter = TransferPeersAdapter(peers.deviceList) {
viewModel.connect(it)
}
}
binding.ripple.startRippleAnimation()
binding.retryButton.setOnClickListener {
viewModel.setStep(TransferStep.TARGET)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,32 +0,0 @@
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import xyz.quaver.pupil.databinding.TransferWaitForConnectionFragmentBinding
class TransferWaitForConnectionFragment : Fragment() {
private var _binding: TransferWaitForConnectionFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = TransferWaitForConnectionFragmentBinding.inflate(layoutInflater)
binding.ripple.startRippleAnimation()
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,149 +0,0 @@
package xyz.quaver.pupil.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF006688)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFC2E8FF)
val md_theme_light_onPrimaryContainer = Color(0xFF001E2B)
val md_theme_light_secondary = Color(0xFF4E616D)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFD1E5F3)
val md_theme_light_onSecondaryContainer = Color(0xFF091E28)
val md_theme_light_tertiary = Color(0xFF5F5A7D)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFE5DEFF)
val md_theme_light_onTertiaryContainer = Color(0xFF1C1736)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFBFCFE)
val md_theme_light_onBackground = Color(0xFF191C1E)
val md_theme_light_surface = Color(0xFFFBFCFE)
val md_theme_light_onSurface = Color(0xFF191C1E)
val md_theme_light_surfaceVariant = Color(0xFFDCE3E9)
val md_theme_light_onSurfaceVariant = Color(0xFF40484D)
val md_theme_light_outline = Color(0xFF71787D)
val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
val md_theme_light_inverseSurface = Color(0xFF2E3133)
val md_theme_light_inversePrimary = Color(0xFF75D1FF)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006688)
val md_theme_light_outlineVariant = Color(0xFFC0C7CD)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFF75D1FF)
val md_theme_dark_onPrimary = Color(0xFF003548)
val md_theme_dark_primaryContainer = Color(0xFF004D67)
val md_theme_dark_onPrimaryContainer = Color(0xFFC2E8FF)
val md_theme_dark_secondary = Color(0xFFB5C9D7)
val md_theme_dark_onSecondary = Color(0xFF20333D)
val md_theme_dark_secondaryContainer = Color(0xFF364954)
val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F3)
val md_theme_dark_tertiary = Color(0xFFC9C2EA)
val md_theme_dark_onTertiary = Color(0xFF312C4C)
val md_theme_dark_tertiaryContainer = Color(0xFF474364)
val md_theme_dark_onTertiaryContainer = Color(0xFFE5DEFF)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF191C1E)
val md_theme_dark_onBackground = Color(0xFFE1E2E5)
val md_theme_dark_surface = Color(0xFF191C1E)
val md_theme_dark_onSurface = Color(0xFFE1E2E5)
val md_theme_dark_surfaceVariant = Color(0xFF40484D)
val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD)
val md_theme_dark_outline = Color(0xFF8A9297)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
val md_theme_dark_inverseSurface = Color(0xFFE1E2E5)
val md_theme_dark_inversePrimary = Color(0xFF006688)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF75D1FF)
val md_theme_dark_outlineVariant = Color(0xFF40484D)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFF4FC3F7)
val Gray50 = Color(0xFFF9FAFB)
val Gray100 = Color(0xFFF3F4F6)
val Gray200 = Color(0xFFE5E7EB)
val Gray300 = Color(0xFFD1D5DB)
val Gray400 = Color(0xFF9CA3AF)
val Gray500 = Color(0xFF6B7280)
val Gray600 = Color(0xFF4B5563)
val Gray700 = Color(0xFF374151)
val Gray800 = Color(0xFF1F2937)
val Gray900 = Color(0xFF111827)
val Red50 = Color(0xFFFEF2F2)
val Red100 = Color(0xFFFEE2E2)
val Red200 = Color(0xFFFECACA)
val Red300 = Color(0xFFFCA5A5)
val Red400 = Color(0xFFF87171)
val Red500 = Color(0xFFEF4444)
val Red600 = Color(0xFFDC2626)
val Red700 = Color(0xFFB91C1C)
val Red800 = Color(0xFF991B1B)
val Red900 = Color(0xFF7F1D1D)
val Yellow50 = Color(0xFFFFFBEB)
val Yellow100 = Color(0xFFFEF3C7)
val Yellow200 = Color(0xFFFDE68A)
val Yellow300 = Color(0xFFFCD34D)
val Yellow400 = Color(0xFFFBBF24)
val Yellow500 = Color(0xFFF59E0B)
val Yellow600 = Color(0xFFD97706)
val Yellow700 = Color(0xFFB45309)
val Yellow800 = Color(0xFF92400E)
val Yellow900 = Color(0xFF78350F)
val Green50 = Color(0xFFECFDF5)
val Green100 = Color(0xFFD1FAE5)
val Green200 = Color(0xFFA7F3D0)
val Green300 = Color(0xFF6EE7B7)
val Green400 = Color(0xFF34D399)
val Green500 = Color(0xFF10B981)
val Green600 = Color(0xFF059669)
val Green700 = Color(0xFF047857)
val Green800 = Color(0xFF065F46)
val Green900 = Color(0xFF064E3B)
val Blue50 = Color(0xFFEFF6FF)
val Blue100 = Color(0xFFDBEAFE)
val Blue200 = Color(0xFFBFDBFE)
val Blue300 = Color(0xFF93C5FD)
val Blue400 = Color(0xFF60A5FA)
val Blue500 = Color(0xFF3B82F6)
val Blue600 = Color(0xFF2563EB)
val Blue700 = Color(0xFF1D4ED8)
val Blue800 = Color(0xFF1E40AF)
val Blue900 = Color(0xFF1E3A8A)
val Indigo50 = Color(0xFFEEF2FF)
val Indigo100 = Color(0xFFE0E7FF)
val Indigo200 = Color(0xFFC7D2FE)
val Indigo300 = Color(0xFFA5B4FC)
val Indigo400 = Color(0xFF818CF8)
val Indigo500 = Color(0xFF6366F1)
val Indigo600 = Color(0xFF4F46E5)
val Indigo700 = Color(0xFF4338CA)
val Indigo800 = Color(0xFF3730A3)
val Indigo900 = Color(0xFF312E81)
val Purple50 = Color(0xFFF5F3FF)
val Purple100 = Color(0xFFEDE9FE)
val Purple200 = Color(0xFFDDD6FE)
val Purple300 = Color(0xFFC4B5FD)
val Purple400 = Color(0xFFA78BFA)
val Purple500 = Color(0xFF8B5CF6)
val Purple600 = Color(0xFF7C3AED)
val Purple700 = Color(0xFF6D28D9)
val Purple800 = Color(0xFF5B21B6)
val Purple900 = Color(0xFF4C1D95)
val Pink50 = Color(0xFFFDF2F8)
val Pink100 = Color(0xFFFCE7F3)
val Pink200 = Color(0xFFFBCFE8)
val Pink300 = Color(0xFFF9A8D4)
val Pink400 = Color(0xFFF472B6)
val Pink500 = Color(0xFFEC4899)
val Pink600 = Color(0xFFDB2777)
val Pink700 = Color(0xFFBE185D)
val Pink800 = Color(0xFF9D174D)
val Pink900 = Color(0xFF831843)

View File

@@ -1,97 +0,0 @@
package xyz.quaver.pupil.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit,
) {
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colors = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> DarkColors
else -> LightColors
}
MaterialTheme(
colorScheme = colors,
content = content
)
}

View File

@@ -25,7 +25,6 @@ import android.graphics.drawable.Animatable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
@@ -59,8 +58,8 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
searchInputView.addTextChangedListener(this) searchInputView.addTextChangedListener(this)
onSearchListener = this onSearchListener = this
onBindSuggestionCallback = { binding, item, itemPosition -> onBindSuggestionCallback = { a, b, c, d, e ->
onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition) onBindSuggestion(a, b, c, d, e)
} }
} }
@@ -76,7 +75,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
s ?: return s ?: return
if (s.any { it.isUpperCase() }) if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().lowercase()) s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
} }
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
@@ -111,7 +110,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
) { ) {
when(item) { when(item) {
is TagSuggestion -> { is TagSuggestion -> {
val tag = "${item.n}:${item.s}" val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon?.setImageDrawable( leftIcon?.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(

View File

@@ -2,14 +2,14 @@ package xyz.quaver.pupil.ui.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import kotlinx.android.synthetic.main.view_progress_card.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) { class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
@@ -29,35 +29,35 @@ class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSe
Type.DOWNLOAD -> R.color.material_green_a700 Type.DOWNLOAD -> R.color.material_green_a700
}.let { }.let {
val color = ContextCompat.getColor(context, it) val color = ContextCompat.getColor(context, it)
DrawableCompat.setTint(binding.progressbar.progressDrawable, color) DrawableCompat.setTint(progressbar.progressDrawable, color)
} }
} }
var progress: Int var progress: Int
get() = binding.progressbar.progress get() = progressbar?.progress ?: 0
set(value) { set(value) {
binding.progressbar.progress = value progressbar?.progress = value
} }
var max: Int var max: Int
get() = binding.progressbar.max get() = progressbar?.max ?: 0
set(value) { set(value) {
binding.progressbar.max = value progressbar?.max = value
binding.progressbar.visibility = progressbar.visibility =
if (value == 0) if (value == 0)
GONE GONE
else else
VISIBLE VISIBLE
} }
val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this)
init { init {
binding.content.setOnClickListener { inflate(context, R.layout.view_progress_card, this)
content.setOnClickListener {
performClick() performClick()
} }
binding.content.setOnLongClickListener { content.setOnLongClickListener {
performLongClick() performLongClick()
} }
} }
@@ -66,7 +66,7 @@ class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSe
if (childCount == 0) if (childCount == 0)
super.addView(child, index, params) super.addView(child, index, params)
else else
binding.content.addView(child, index, params) content.addView(child, index, params)
} }
} }

View File

@@ -1,12 +0,0 @@
package xyz.quaver.pupil.ui.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SearchState())
val searchState: StateFlow<SearchState> = _uiState
}
data class SearchState(val stub: String = "")

View File

@@ -18,13 +18,11 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.serialization.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import java.io.File import java.io.File
import java.util.*
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set { class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@@ -48,8 +46,6 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
Json.decodeFromString(serializer, file.readText()) Json.decodeFromString(serializer, file.readText())
}.onSuccess { }.onSuccess {
set.addAll(it) set.addAll(it)
}.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
} }
} }
@@ -61,6 +57,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
@Synchronized @Synchronized
override fun add(element: T): Boolean { override fun add(element: T): Boolean {
load()
set.remove(element) set.remove(element)
return set.add(element).also { return set.add(element).also {
@@ -70,6 +68,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
@Synchronized @Synchronized
override fun addAll(elements: Collection<T>): Boolean { override fun addAll(elements: Collection<T>): Boolean {
load()
set.removeAll(elements) set.removeAll(elements)
return set.addAll(elements).also { return set.addAll(elements).also {
@@ -79,6 +79,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
@Synchronized @Synchronized
override fun remove(element: T): Boolean { override fun remove(element: T): Boolean {
load()
return set.remove(element).also { return set.remove(element).also {
save() save()
} }

View File

@@ -21,95 +21,39 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Request import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.* import xyz.quaver.io.util.*
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.hitomi.* import xyz.quaver.pupil.util.Preferences
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@Serializable
data class OldReader(
val code: String,
val galleryInfo: OldGalleryInfo
)
@Serializable
data class OldGalleryInfo(
val language_localname: String? = null,
val language: String? = null,
val date: String? = null,
val files: List<OldGalleryFiles>,
val id: Int? = null,
val type: String? = null,
val title: String? = null
)
@Serializable
data class OldGalleryFiles(
val width: Int,
val hash: String,
val haswebp: Int = 0,
val name: String,
val height: Int,
val hasavif: Int = 0,
val hasavifsmalltn: Int? = 0
)
@Serializable
data class OldMetadata(
var galleryBlock: GalleryBlock? = null,
var reader: OldReader? = null,
var imageList: MutableList<String?>? = null
) {
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
@Serializable @Serializable
data class Metadata( data class Metadata(
var galleryBlock: GalleryBlock? = null, var galleryBlock: GalleryBlock? = null,
var galleryInfo: GalleryInfo? = null, var reader: Reader? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
constructor(old: OldMetadata) : this( fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
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] } })
} }
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>()
@@ -130,14 +74,10 @@ 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 {
kotlin.runCatching { Json.decodeFromString<Metadata>(it)
Json.decodeFromString<Metadata>(metadata)
}.getOrElse {
Metadata(json.decodeFromString<OldMetadata>(metadata))
}
} }
}.onFailure { it.printStackTrace() }.getOrNull() ?: Metadata() }.getOrNull() ?: Metadata()
val downloadFolder: FileX? val downloadFolder: FileX?
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID) get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
@@ -170,13 +110,27 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
} }
suspend fun getGalleryBlock(): GalleryBlock? { suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
return metadata.galleryBlock return metadata.galleryBlock
?: withContext(Dispatchers.IO) { ?: withContext(Dispatchers.IO) {
try { var galleryBlock: GalleryBlock? = null
getGalleryBlock(galleryID).also {
setMetadata { metadata -> metadata.galleryBlock = it } for (source in sources) {
} galleryBlock = try {
} catch (e: Exception) { return@withContext null } source.invoke()
} catch (e: Exception) { null }
if (galleryBlock != null)
break
}
galleryBlock?.also {
setMetadata { metadata -> metadata.galleryBlock = it }
}
} }
} }
@@ -187,7 +141,6 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
kotlin.runCatching { kotlin.runCatching {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)
.header("Referer", "https://hitomi.la/")
.build() .build()
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
@@ -201,21 +154,41 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}.getOrNull()?.uri } }.getOrNull()?.uri }
} } ?: Uri.EMPTY } } ?: Uri.EMPTY
suspend fun getGalleryInfo(): GalleryInfo? { suspend fun getReader(): Reader? {
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
return metadata.galleryInfo val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
else
it
}
return metadata.reader
?: withContext(Dispatchers.IO) { ?: withContext(Dispatchers.IO) {
try { var reader: Reader? = null
getGalleryInfo(galleryID).also {
setMetadata { metadata ->
metadata.galleryInfo = it
if (metadata.imageList == null) for (source in sources) {
metadata.imageList = MutableList(it.files.size) { null } reader = try {
} source.value.invoke()
} catch (e: Exception) {
null
}
if (reader != null)
break
}
reader?.also {
setMetadata { metadata ->
metadata.reader = it
if (metadata.imageList == null)
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
} }
} catch (e: Exception) {
null
} }
} }
} }
@@ -224,12 +197,11 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.imageList?.getOrNull(index)?.let { findFile(it) } metadata.imageList?.getOrNull(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun putImage(index: Int, fileName: String, data: ByteArray) = coroutineScope { fun putImage(index: Int, fileName: String, data: ByteArray) {
val file = cacheFolder.getChild(fileName) val file = cacheFolder.getChild(fileName)
if (!file.exists()) if (!file.exists())
file.createNewFile() file.createNewFile()
file.writeBytes(data) file.writeBytes(data)
setMetadata { metadata -> metadata.imageList!![index] = fileName } setMetadata { metadata -> metadata.imageList!![index] = fileName }
} }

View File

@@ -21,18 +21,13 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Log import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Call import okhttp3.Call
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.deleteRecursively import xyz.quaver.io.util.*
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
@@ -52,12 +47,14 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!) val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder: FileX val downloadFolder: FileX
get() = kotlin.runCatching { get() = {
FileX(this, Preferences.get<String>("download_folder")) kotlin.runCatching {
}.getOrElse { FileX(this, Preferences.get<String>("download_folder"))
Preferences["download_folder"] = defaultDownloadFolder.uri.toString() }.getOrElse {
defaultDownloadFolder Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
} defaultDownloadFolder
}
}.invoke()
private var prevDownloadFolder: FileX? = null private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap<Int, String>? = null private var downloadFolderMapInstance: MutableMap<Int, String>? = null
@@ -66,19 +63,21 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
get() { get() {
if (prevDownloadFolder != downloadFolder) { if (prevDownloadFolder != downloadFolder) {
prevDownloadFolder = downloadFolder prevDownloadFolder = downloadFolder
downloadFolderMapInstance = run { downloadFolderMapInstance = {
val file = downloadFolder.getChild(".download") val file = downloadFolder.getChild(".download")
val data = if (file.exists()) val data = if (file.exists())
kotlin.runCatching { kotlin.runCatching {
file.readText()?.let{ Json.decodeFromString<MutableMap<Int, String>>(it) } file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) }
}.onFailure { file.delete() }.getOrNull() }.onFailure { file.delete() }.getOrNull()
else else
null null
data ?: run {
data ?: {
file.createNewFile() file.createNewFile()
mutableMapOf() mutableMapOf<Int, String>()
} }.invoke()
} }.invoke()
} }
return downloadFolderMapInstance ?: mutableMapOf() return downloadFolderMapInstance ?: mutableMapOf()
@@ -87,7 +86,7 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
@Synchronized @Synchronized
fun isDownloading(galleryID: Int): Boolean { fun isDownloading(galleryID: Int): Boolean {
val isThisGallery: (Call) -> Boolean = { !it.isCanceled && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
return downloadFolderMap.containsKey(galleryID) return downloadFolderMap.containsKey(galleryID)
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) } && client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
@@ -97,19 +96,23 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
fun getDownloadFolder(galleryID: Int): FileX? = fun getDownloadFolder(galleryID: Int): FileX? =
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) } downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { @Synchronized
val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock() fun addDownloadFolder(galleryID: Int) {
?.formatDownloadFolder() ?: return@launch val name = runBlocking {
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
}?.formatDownloadFolder() ?: return
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
downloadFolderMap[galleryID] = name if (folder.exists())
return
folder.mkdir()
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() } downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap)) downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
if (folder.exists()) return@launch
folder.mkdir()
} }
@Synchronized @Synchronized

View File

@@ -18,37 +18,30 @@
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.Context
import android.content.pm.PackageManager import android.content.Intent
import android.os.Build 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 androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlinx.serialization.json.*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.pupil.R import xyz.quaver.Code
import xyz.quaver.pupil.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.pupil.hitomi.GalleryInfo import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.hitomi.imageUrlFromImage import xyz.quaver.hitomi.getReferer
import java.util.Locale import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.createImgList
import java.util.*
import kotlin.collections.ArrayList
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
fun String.wordCapitalize(): String { fun String.wordCapitalize() : String {
val result = ArrayList<String>() val result = ArrayList<String>()
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
for (word in this.split(" ")) for (word in this.split(" "))
result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }) result.add(word.capitalize(Locale.US))
return result.joinToString(" ") return result.joinToString(" ")
} }
@@ -61,9 +54,8 @@ private val suffix = listOf(
"TB" //really? "TB" //really?
) )
fun byteToString(byte: Long, precision: Int = 1): String { fun byteToString(byte: Long, precision : Int = 1) : String {
var size = byte.toDouble() var size = byte.toDouble(); var suffixIndex = 0
var suffixIndex = 0
while (size >= 1024) { while (size >= 1024) {
size /= 1024 size /= 1024
@@ -91,11 +83,9 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
val formatMap = mapOf<String, GalleryBlock.() -> (String)>( val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
"-id-" to { id.toString() }, "-id-" to { id.toString() },
"-title-" to { title }, "-title-" to { title },
"-artist-" to { if (artists.isNotEmpty()) artists.joinToString() else "N/A" }, "-artist-" to { artists.joinToString() }
"-group-" to { if (groups.isNotEmpty()) groups.joinToString() else "N/A" }
// TODO // TODO
) )
/** /**
* Formats download folder name with given Metadata * Formats download folder name with given Metadata
*/ */
@@ -113,49 +103,33 @@ fun GalleryBlock.formatDownloadFolderTest(format: String): String =
} }
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127) }.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> { val Reader.requestBuilders: List<Request.Builder>
val galleryID = this.id.toIntOrNull() ?: 0 get() {
return this.files.map { val galleryID = this.galleryInfo.id ?: 0
Request.Builder() val lowQuality = Preferences["low_quality", true]
.url(
runCatching { return when(code) {
imageUrlFromImage(galleryID, it, false) Code.HITOMI -> {
this.galleryInfo.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
} }
.onFailure { }
FirebaseCrashlytics.getInstance().recordException(it) Code.HIYOBI -> {
} createImgList(galleryID, this, lowQuality).map {
.getOrDefault("https://a/") Request.Builder()
) .url(it.path)
.header("Referer", "https://hitomi.la/") }
} }
}
fun byteCount(codePoint: Int): Int = when (codePoint) {
in 0..<0x80 -> 1
in 0x80..<0x800 -> 2
in 0x800..<0x10000 -> 3
in 0x10000..<0x110000 -> 4
else -> 0
}
fun String.ellipsize(n: Int): String = buildString {
var count = 0
var index = 0
val codePointLength = this@ellipsize.codePointCount(0, this@ellipsize.length)
while (index < codePointLength) {
val nextCodePoint = this@ellipsize.codePointAt(index)
val nextByte = byteCount(nextCodePoint)
if (count + nextByte > 124) {
append("")
break
} }
appendCodePoint(nextCodePoint)
count += nextByte
index++
} }
} fun String.ellipsize(n: Int): String =
if (this.length > n)
this.slice(0 until n) + ""
else
this
operator fun JsonElement.get(index: Int) = operator fun JsonElement.get(index: Int) =
this.jsonArray[index] this.jsonArray[index]
@@ -163,43 +137,5 @@ operator fun JsonElement.get(index: Int) =
operator fun JsonElement.get(tag: String) = operator fun JsonElement.get(tag: String) =
this.jsonObject[tag] this.jsonObject[tag]
fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
this.jsonObject.getOrDefault(tag, null)
}.getOrNull()
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

@@ -18,23 +18,49 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.util.Base64
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
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.* import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.pupil.types.Tag import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@@ -135,11 +161,6 @@ 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
@@ -161,10 +182,10 @@ fun checkUpdate(context: Context, force: Boolean = false) {
Preferences["update_download_id"] = it Preferences["update_download_id"] = it
} }
} }
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ -> setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ ->
if (!force) if (!force)
preferences.edit() preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 86400000) .putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.apply() .apply()
} }
} }
@@ -175,7 +196,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Int) -> Unit)? = null) { fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -193,22 +214,134 @@ 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 {
val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]") Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
favorites.addAll(it)
when (data) { onSuccess?.invoke(it)
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) }
} }
}) })
}
private var job: Job? = null
private val receiver = object: BroadcastReceiver() {
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
ACTION_CANCEL -> {
job?.cancel()
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
context.unregisterReceiver(this)
}
}
}
}
@SuppressLint("RestrictedApi")
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
val notificationManager = NotificationManagerCompat.from(this)
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
).build()
val notification = NotificationCompat.Builder(this, "import")
.setContentTitle(getText(R.string.import_old_galleries_notification))
.setProgress(0, 0, true)
.addAction(action)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
DownloadService.cancel(this)
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
val images = listOf(
"jpg",
"png",
"gif",
"webp"
)
val downloadFolders = downloadFolder.listFiles { folder ->
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
}?.map {
if (it !is FileX)
FileX(this@migrate, it)
else
it
}
if (downloadFolders.isNullOrEmpty()) return@launch
downloadFolders.forEachIndexed { index, folder ->
notification
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
.setProgress(index, downloadFolders.size, false)
notificationManager.notify(R.id.notification_id_import, notification.build())
val metadata = kotlin.runCatching {
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it) }
}.getOrNull()
val galleryID = metadata?.get("reader")?.get("galleryInfo")?.get("id")?.content?.toIntOrNull()
?: folder.name.toIntOrNull() ?: return@forEachIndexed
val galleryBlock: GalleryBlock? = kotlin.runCatching {
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
}.getOrNull() ?: kotlin.runCatching {
getGalleryBlock(galleryID)
}.getOrNull() ?: kotlin.runCatching {
xyz.quaver.hiyobi.getGalleryBlock(galleryID)
}.getOrNull()
val reader: Reader? = kotlin.runCatching {
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
}.getOrNull() ?: kotlin.runCatching {
getReader(galleryID)
}.getOrNull() ?: kotlin.runCatching {
xyz.quaver.hiyobi.getReader(galleryID)
}.getOrNull()
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
val file = folder.getChild(".thumbnail").also {
if (it.exists())
it.delete()
it.createNewFile()
}
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
}
val list: MutableList<String?> =
MutableList(reader!!.galleryInfo.files.size) { null }
folder.list { _, name ->
name?.substringAfterLast('.') in images
}?.sorted()?.take(list.size)?.forEachIndexed { i, name ->
list[i] = name
}
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
Json.encodeToString(Metadata(galleryBlock, reader, list))
)
Cache.delete(this@migrate, galleryID)
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
}
notification
.setContentText(getText(R.string.import_old_galleries_notification_done))
.setProgress(0, 0, false)
.setOngoing(false)
.mActions.clear()
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
unregisterReceiver(receiver)
}
}
} }

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.5,10.5 L12,3m0,0 l7.5,7.5M12,3v18"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorPrimaryDark"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9,12.75 L11.25,15 15,9.75M21,12a9,9 0,1 1,-18 0,9 9,0 0,1 18,0Z"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorPrimaryDark"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3,16.5v2.25A2.25,2.25 0,0 0,5.25 21h13.5A2.25,2.25 0,0 0,21 18.75V16.5M16.5,12 L12,16.5m0,0L7.5,12m4.5,4.5V3"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/material_blue_700"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21,8.25c0,-2.485 -2.099,-4.5 -4.688,-4.5 -1.935,0 -3.597,1.126 -4.312,2.733 -0.715,-1.607 -2.377,-2.733 -4.313,-2.733C5.1,3.75 3,5.765 3,8.25c0,7.22 9,12 9,12s9,-4.78 9,-12Z"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/material_pink_600"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/material_orange_500"
android:pathData="M477,840q-142,0 -243.5,-95.5T121,509q-1,-12 7.5,-21t21.5,-9q12,0 20.5,8.5T181,509q11,115 95,193t201,78q127,0 215,-89t88,-216q0,-124 -89,-209.5T477,180q-68,0 -127.5,31T246,293h75q13,0 21.5,8.5T351,323q0,13 -8.5,21.5T321,353L172,353q-13,0 -21.5,-8.5T142,323v-148q0,-13 8.5,-21.5T172,145q13,0 21.5,8.5T202,175v76q52,-61 123.5,-96T477,120q75,0 141,28t115.5,76.5Q783,273 811.5,338T840,478q0,75 -28.5,141t-78,115Q684,783 618,811.5T477,840ZM511,466 L626,579q9,9 9,21.5t-9,21.5q-9,9 -21,9t-21,-9L460,500q-5,-5 -7,-10.5t-2,-11.5v-171q0,-13 8.5,-21.5T481,277q13,0 21.5,8.5T511,307v159Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.19,8.688a4.5,4.5 0,0 1,1.242 7.244l-4.5,4.5a4.5,4.5 0,0 1,-6.364 -6.364l1.757,-1.757m13.35,-0.622 l1.757,-1.757a4.5,4.5 0,0 0,-6.364 -6.364l-4.5,4.5a4.5,4.5 0,0 0,1.242 7.244"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorPrimaryDark"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false">
<shape android:shape="oval">
<solid android:color="#dddddd"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#888888"/>
</shape>
</item>
</selector>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:bottom="-8dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="32dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp" />
<stroke android:width="4dp" android:color="#444444"/>
</shape>
</item>
</layer-list>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#9d9d9d">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#9d9d9d" />
<corners android:radius="16dp" />
</shape>
</item>
</ripple>

View File

@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,9v3.75m-9.303,3.376c-0.866,1.5 0.217,3.374 1.948,3.374h14.71c1.73,0 2.813,-1.874 1.948,-3.374L13.949,3.378c-0.866,-1.5 -3.032,-1.5 -3.898,0L2.697,16.126ZM12,15.75h0.007v0.008H12v-0.008Z"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorAccent"
android:strokeLineCap="round"/>
</vector>

View File

@@ -41,7 +41,7 @@
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"> app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fingerprint_btn" android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint" app:srcCompat="@drawable/fingerprint"
@@ -64,7 +64,7 @@
android:gravity="center"> android:gravity="center">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/pattern_btn" android:id="@+id/lock_pattern"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"
@@ -73,7 +73,7 @@
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/pin_btn" android:id="@+id/lock_pin"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"
@@ -84,7 +84,7 @@
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/password_btn" android:id="@+id/lock_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"

View File

@@ -21,18 +21,17 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer" android:id="@+id/main_drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:openDrawer="start"> tools:openDrawer="start">
<include android:id="@+id/contents" <include layout="@layout/activity_main_content"
layout="@layout/main_activity_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view" android:id="@+id/main_nav_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079 ~ Copyright (C) 2020 tom5079
~ ~
@@ -17,60 +18,61 @@
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.MainActivity"> tools:context=".ui.MainActivity">
<xyz.quaver.pupil.ui.view.MainView <xyz.quaver.pupil.ui.view.MainView
android:id="@+id/view" android:id="@+id/main_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller <com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:addLastItemPadding="true"
app:handleDrawable="@drawable/thumb" app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true" app:handleHasFixedSize="true"
app:handleHeight="72dp" app:handleHeight="72dp"
app:handleVisibilityDuration="1000"
app:handleWidth="24dp" app:handleWidth="24dp"
app:popupDrawable="@android:color/transparent" app:disableTrack="true"
app:trackMarginStart="64dp"> app:hideHandleAfter="1000"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview" android:id="@+id/main_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="64dp" android:paddingTop="64dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller> </com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView> </xyz.quaver.pupil.ui.view.MainView>
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/progressbar"
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
android:id="@+id/main_progressbar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminate="true" /> android:indeterminate="true"/>
<TextView <TextView
android:id="@+id/noresult" android:id="@+id/main_noresult"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:linksClickable="true"
android:text="@string/main_no_result" android:text="@string/main_no_result"
android:visibility="invisible" /> android:linksClickable="true"
android:visibility="invisible"/>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab" android:id="@+id/main_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
@@ -78,48 +80,48 @@
app:menu_colorNormal="@color/colorAccent"> app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/cancel_fab" android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel" app:fab_label="@string/main_fab_cancel"
app:fab_size="mini" /> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/jump_fab" android:id="@+id/main_fab_jump"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title" app:fab_label="@string/main_jump_title"
app:fab_size="mini" /> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/random_fab" android:id="@+id/main_fab_random"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random" app:fab_label="@string/main_fab_random"
app:fab_size="mini" /> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/id_fab" android:id="@+id/main_fab_id"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id" app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini" /> app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu> </com.github.clans.fab.FloatingActionMenu>
<xyz.quaver.pupil.ui.view.FloatingSearchView <xyz.quaver.pupil.ui.view.FloatingSearchView
android:id="@+id/searchview" android:id="@+id/main_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:close_search_on_keyboard_dismiss="false"
app:dismissOnOutsideTouch="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:searchBarMarginLeft="6dp" app:searchBarMarginLeft="6dp"
app:searchBarMarginRight="6dp" app:searchBarMarginRight="6dp"
app:searchBarMarginTop="6dp" app:searchBarMarginTop="6dp"
app:searchHint="@string/search_hint" app:searchHint="@string/search_hint"
app:suggestionAnimDuration="250"
app:showSearchKey="true" app:showSearchKey="true"
app:suggestionAnimDuration="250" /> app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:dismissOnOutsideTouch="true"
app:close_search_on_keyboard_dismiss="false" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079 ~ Copyright (C) 2019 tom5079
~ ~
@@ -17,6 +18,7 @@
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reader_layout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -28,39 +30,39 @@
android:id="@+id/scroller" android:id="@+id/scroller"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:addLastItemPadding="true"
app:handleDrawable="@drawable/thumb" app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp" app:handleHeight="72dp"
app:handleVisibilityDuration="1000"
app:handleWidth="24dp" app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:handleHasFixedSize="true"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent"> app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview" android:id="@+id/reader_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller> </com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<include <include layout="@layout/reader_eye_card"
android:id="@+id/eye_card" android:id="@+id/eye_card"
layout="@layout/reader_eye_card" android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_margin="8dp" android:layout_margin="8dp"/>
android:visibility="gone" />
<ProgressBar <ProgressBar
android:id="@+id/download_progressbar" android:id="@+id/reader_download_progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" /> android:layout_height="4dp"/>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab" android:id="@+id/reader_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
@@ -68,36 +70,36 @@
app:menu_colorNormal="@color/colorAccent"> app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/download_fab" android:id="@+id/reader_fab_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_download"
app:fab_label="@string/reader_fab_download" app:fab_label="@string/reader_fab_download"
app:fab_size="mini" app:fab_size="mini"/>
app:srcCompat="@drawable/ic_download" />
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/retry_fab" android:id="@+id/reader_fab_retry"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/refresh"
app:fab_label="@string/reader_fab_retry" app:fab_label="@string/reader_fab_retry"
app:fab_size="mini" app:fab_size="mini"/>
app:srcCompat="@drawable/refresh" />
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/auto_fab" android:id="@+id/reader_fab_auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/eye_white"
app:fab_label="@string/reader_fab_auto" app:fab_label="@string/reader_fab_auto"
app:fab_size="mini" app:fab_size="mini"/>
app:srcCompat="@drawable/eye_white" />
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/fullscreen_fab" android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen" app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini" app:fab_size="mini"/>
app:srcCompat="@drawable/ic_fullscreen" />
</com.github.clans.fab.FloatingActionMenu> </com.github.clans.fab.FloatingActionMenu>

View File

@@ -28,7 +28,7 @@
tools:ignore="Autofill" tools:ignore="Autofill"
android:inputType="text" android:inputType="text"
android:hint="@string/settings_default_query" android:hint="@string/settings_default_query"
android:id="@+id/edittext" android:id="@+id/default_query_dialog_edittext"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -36,10 +36,10 @@
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout <LinearLayout
android:id="@+id/language_layout" android:id="@+id/default_query_dialog_language_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/edittext" app:layout_constraintTop_toBottomOf="@id/default_query_dialog_edittext"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -49,14 +49,14 @@
android:text="@string/default_query_dialog_language"/> android:text="@string/default_query_dialog_language"/>
<Spinner <Spinner
android:id="@+id/language_selector" android:id="@+id/default_query_dialog_language_selector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/BL_layout" android:id="@+id/default_query_dialog_BL_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -64,7 +64,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/language_layout" app:layout_constraintTop_toBottomOf="@id/default_query_dialog_language_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -75,14 +75,14 @@
android:text="@string/default_query_dialog_filter_BL"/> android:text="@string/default_query_dialog_filter_BL"/>
<CheckBox <CheckBox
android:id="@+id/BL_checkbox" android:id="@+id/default_query_dialog_BL_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/guro_layout" android:id="@+id/default_query_dialog_guro_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -90,7 +90,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/BL_layout" app:layout_constraintTop_toBottomOf="@id/default_query_dialog_BL_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -101,7 +101,7 @@
android:text="@string/default_query_dialog_filter_guro"/> android:text="@string/default_query_dialog_filter_guro"/>
<CheckBox <CheckBox
android:id="@+id/guro_checkbox" android:id="@+id/default_query_dialog_guro_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@@ -115,7 +115,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/guro_layout" app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -126,7 +126,7 @@
android:text="@string/default_query_dialog_filter_loli"/> android:text="@string/default_query_dialog_filter_loli"/>
<CheckBox <CheckBox
android:id="@+id/loli_checkbox" android:id="@+id/default_query_dialog_loli_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

View File

@@ -19,13 +19,14 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gallery_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar" android:id="@+id/gallery_toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -40,34 +41,34 @@
android:padding="8dp"> android:padding="8dp">
<com.github.piasy.biv.view.BigImageView <com.github.piasy.biv.view.BigImageView
android:id="@+id/cover" android:id="@+id/gallery_cover"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/title" app:layout_constraintRight_toLeftOf="@id/gallery_title"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/gallery_title"
style="@style/TextAppearance.AppCompat.Headline" style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/cover" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
<TextView <TextView
style="@style/TextAppearance.AppCompat.Medium" style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/artist" android:id="@+id/gallery_artist"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintTop_toBottomOf="@id/gallery_title"
app:layout_constraintLeft_toRightOf="@id/cover" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -75,15 +76,15 @@
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/artist" app:layout_constraintTop_toBottomOf="@id/gallery_artist"
app:layout_constraintBottom_toTopOf="@id/type"/> app:layout_constraintBottom_toTopOf="@id/gallery_type"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/type" android:id="@+id/gallery_type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/cover" app:layout_constraintLeft_toRightOf="@id/gallery_cover"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -99,7 +100,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
android:id="@+id/contents" android:id="@+id/gallery_contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"/> android:orientation="vertical"/>
@@ -111,7 +112,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<ProgressBar <ProgressBar
android:id="@+id/progressbar" android:id="@+id/gallery_progressbar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@@ -122,11 +123,11 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/gallery_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
app:layout_anchor="@id/toolbar" app:layout_anchor="@id/gallery_toolbar"
app:layout_anchorGravity="bottom|end"/> app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -25,14 +25,14 @@
android:padding="8dp"> android:padding="8dp">
<TextView <TextView
android:id="@+id/type" android:id="@+id/gallery_details"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1" style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"/> android:textColor="@color/colorAccent"/>
<LinearLayout <LinearLayout
android:id="@+id/contents" android:id="@+id/gallery_details_contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"/> android:orientation="vertical"/>

Some files were not shown because too many files have changed in this diff Show More