WIP
This commit is contained in:
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="1.8" />
|
<bytecodeTargetLevel target="11" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<targetSelectedWithDropDown>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_30_x86.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
</targetSelectedWithDropDown>
|
||||||
|
<timeTargetWasSelectedWithDropDown value="2021-09-15T00:09:20.844719Z" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -7,6 +7,7 @@
|
|||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="JDK" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|||||||
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
34
.idea/misc.xml
generated
34
.idea/misc.xml
generated
@@ -1,6 +1,38 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="DesignSurface">
|
||||||
|
<option name="filePathToZoomLevelMap">
|
||||||
|
<map>
|
||||||
|
<entry key="../../../../layout/compose-model-1627195341053.xml" value="0.33" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627257664594.xml" value="0.4518581081081081" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627353146836.xml" value="0.5" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627359089674.xml" value="0.67" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627469604886.xml" value="0.3684210526315789" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627528986080.xml" value="0.13723644578313254" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627529731737.xml" value="0.18919427710843373" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627530302667.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627605645856.xml" value="0.25" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627688771576.xml" value="0.3023525994772001" />
|
||||||
|
<entry key="../../../../layout/compose-model-1627721024779.xml" value="0.3020621550972989" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628033383820.xml" value="0.23796296296296296" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628120781047.xml" value="0.28405460354342144" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628214547556.xml" value="0.2939297124600639" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628301117560.xml" value="0.18711713384072767" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628301166312.xml" value="0.19250046408019306" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628490334478.xml" value="0.1212177464265825" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628898655628.xml" value="0.19300204727340406" />
|
||||||
|
<entry key="../../../../layout/compose-model-1628898937985.xml" value="0.19300204727340406" />
|
||||||
|
<entry key="../../../../layout/compose-model-1631666404391.xml" value="0.36203703703703705" />
|
||||||
|
<entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" />
|
||||||
|
<entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" />
|
||||||
|
<entry key="app/src/main/res/layout/main_activity.xml" value="0.2953125" />
|
||||||
|
<entry key="app/src/main/res/layout/main_activity_content.xml" value="0.2953125" />
|
||||||
|
<entry key="app/src/main/res/layout/progress_card_view.xml" value="0.2953125" />
|
||||||
|
<entry key="app/src/main/res/layout/search_result_item.xml" value="0.2489868287740628" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ plugins {
|
|||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("kotlinx-serialization")
|
id("kotlinx-serialization")
|
||||||
id("com.google.android.gms.oss-licenses-plugin")
|
id("com.google.android.gms.oss-licenses-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
if (File("google-services.json").exists()) {
|
id("com.google.firebase.crashlytics")
|
||||||
println("Firebase Enabled")
|
id("com.google.firebase.firebase-perf")
|
||||||
id("com.google.gms.google-services")
|
|
||||||
id("com.google.firebase.crashlytics")
|
|
||||||
id("com.google.firebase.firebase-perf")
|
|
||||||
} else {
|
|
||||||
println("Firebase Disabled")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -41,7 +35,10 @@ android {
|
|||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
|
|
||||||
|
isCrunchPngs = false
|
||||||
|
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -49,45 +46,61 @@ android {
|
|||||||
dataBinding = true
|
dataBinding = true
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
composeOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
kotlinCompilerExtensionVersion = "1.0.0"
|
||||||
freeCompilerArgs = listOf("-Xuse-experimental=kotlin.Experimental")
|
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
|
||||||
|
|
||||||
implementation("androidx.compose.ui:ui:1.0.0-rc02")
|
implementation("androidx.compose.ui:ui:1.0.2")
|
||||||
implementation("androidx.compose.ui:ui-tooling:1.0.0-rc02")
|
implementation("androidx.compose.ui:ui-tooling:1.0.2")
|
||||||
implementation("androidx.compose.foundation:foundation:1.0.0-rc02")
|
implementation("androidx.compose.foundation:foundation:1.0.2")
|
||||||
implementation("androidx.compose.material:material:1.0.0-rc02")
|
implementation("androidx.compose.material:material:1.0.2")
|
||||||
implementation("androidx.compose.material:material-icons-core:1.0.0-rc02")
|
implementation("androidx.compose.material:material-icons-core:1.0.2")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.0.0-rc02")
|
implementation("androidx.compose.material:material-icons-extended:1.0.2")
|
||||||
implementation("androidx.compose.runtime:runtime-livedata:1.0.0-rc02")
|
implementation("androidx.compose.runtime:runtime-livedata:1.0.2")
|
||||||
|
implementation("androidx.compose.material:material-icons-extended:1.0.2")
|
||||||
|
implementation("androidx.activity:activity-compose:1.3.1")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-core:1.6.1")
|
implementation("com.google.accompanist:accompanist-flowlayout:0.16.1")
|
||||||
implementation("io.ktor:ktor-client-okhttp:1.6.1")
|
implementation("com.google.accompanist:accompanist-appcompat-theme:0.16.0")
|
||||||
implementation("io.ktor:ktor-client-serialization:1.6.1")
|
|
||||||
|
implementation("io.coil-kt:coil-compose:1.3.2")
|
||||||
|
|
||||||
|
implementation("io.ktor:ktor-client-core:1.6.3")
|
||||||
|
implementation("io.ktor:ktor-client-okhttp:1.6.3")
|
||||||
|
implementation("io.ktor:ktor-client-serialization:1.6.3")
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||||
implementation("androidx.activity:activity-ktx:1.3.0-rc02")
|
implementation("androidx.activity:activity-ktx:1.3.1")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||||
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta02")
|
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
|
||||||
|
|
||||||
implementation("org.kodein.di:kodein-di-framework-android-x:7.6.0")
|
implementation("androidx.room:room-runtime:2.3.0")
|
||||||
|
annotationProcessor("androidx.room:room-compiler:2.3.0")
|
||||||
|
kapt("androidx.room:room-compiler:2.3.0")
|
||||||
|
implementation("androidx.room:room-ktx:2.3.0")
|
||||||
|
|
||||||
|
implementation("org.kodein.di:kodein-di-framework-compose:7.7.0")
|
||||||
|
|
||||||
implementation("com.daimajia.swipelayout:library:1.2.0@aar")
|
implementation("com.daimajia.swipelayout:library:1.2.0@aar")
|
||||||
|
|
||||||
@@ -104,9 +117,9 @@ dependencies {
|
|||||||
|
|
||||||
//implementation("com.quiph.ui:recyclerviewfastscroller:0.2.1")
|
//implementation("com.quiph.ui:recyclerviewfastscroller:0.2.1")
|
||||||
|
|
||||||
implementation("com.github.piasy:BigImageViewer:1.8.0")
|
implementation("com.github.piasy:BigImageViewer:1.8.1")
|
||||||
implementation("com.github.piasy:FrescoImageLoader:1.8.0")
|
implementation("com.github.piasy:FrescoImageLoader:1.8.1")
|
||||||
implementation("com.github.piasy:FrescoImageViewFactory:1.8.0")
|
implementation("com.github.piasy:FrescoImageViewFactory:1.8.1")
|
||||||
|
|
||||||
implementation("org.jsoup:jsoup:1.14.1")
|
implementation("org.jsoup:jsoup:1.14.1")
|
||||||
|
|
||||||
@@ -117,20 +130,20 @@ dependencies {
|
|||||||
|
|
||||||
implementation("ru.noties.markwon:core:3.1.0")
|
implementation("ru.noties.markwon:core:3.1.0")
|
||||||
|
|
||||||
implementation("xyz.quaver:libpupil:2.1.3")
|
implementation("xyz.quaver:libpupil:2.1.6")
|
||||||
implementation("xyz.quaver:documentfilex:0.6.1")
|
implementation("xyz.quaver:documentfilex:0.6.1")
|
||||||
implementation("xyz.quaver:floatingsearchview:1.1.7")
|
implementation("xyz.quaver:floatingsearchview:1.1.7")
|
||||||
|
|
||||||
debugImplementation("com.orhanobut:logger:2.2.0")
|
implementation("org.kodein.log:kodein-log:0.11.1")
|
||||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
testImplementation("org.mockito:mockito-inline:3.12.4")
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||||
androidTestImplementation("androidx.test:runner:1.4.0")
|
androidTestImplementation("androidx.test:runner:1.4.0")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0-rc02")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.0-alpha03")
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,6 @@ import com.google.firebase.analytics.FirebaseAnalytics
|
|||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.orhanobut.logger.AndroidLogAdapter
|
|
||||||
import com.orhanobut.logger.Logger
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.*
|
||||||
@@ -47,6 +45,7 @@ import io.ktor.client.features.json.serializer.*
|
|||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import org.kodein.di.android.x.androidXModule
|
import org.kodein.di.android.x.androidXModule
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
|
import xyz.quaver.pupil.db.databaseModule
|
||||||
import xyz.quaver.pupil.sources.sourceModule
|
import xyz.quaver.pupil.sources.sourceModule
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -56,16 +55,11 @@ class Pupil : Application(), DIAware {
|
|||||||
|
|
||||||
override val di: DI by DI.lazy {
|
override val di: DI by DI.lazy {
|
||||||
import(androidXModule(this@Pupil))
|
import(androidXModule(this@Pupil))
|
||||||
|
import(databaseModule)
|
||||||
import(sourceModule)
|
import(sourceModule)
|
||||||
|
|
||||||
bind { singleton { ImageCache(applicationContext) } }
|
|
||||||
bind { singleton { DownloadManager(applicationContext) } }
|
bind { singleton { DownloadManager(applicationContext) } }
|
||||||
|
|
||||||
bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "histories.json")) }
|
|
||||||
bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favorites.json")) }
|
|
||||||
bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favoriteTags.json")) }
|
|
||||||
bind<SavedSourceSet>(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "searchHistory.json")) }
|
|
||||||
|
|
||||||
bind { singleton {
|
bind { singleton {
|
||||||
HttpClient(OkHttp) {
|
HttpClient(OkHttp) {
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
@@ -90,8 +84,6 @@ class Pupil : Application(), DIAware {
|
|||||||
firebaseAnalytics = Firebase.analytics
|
firebaseAnalytics = Firebase.analytics
|
||||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||||
|
|
||||||
Logger.addLogAdapter(AndroidLogAdapter())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Preferences.get<String>("download_folder").also {
|
Preferences.get<String>("download_folder").also {
|
||||||
if (it.startsWith("content"))
|
if (it.startsWith("content"))
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 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.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.daimajia.swipe.SwipeLayout
|
|
||||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
|
||||||
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco
|
|
||||||
import com.facebook.drawee.controller.BaseControllerListener
|
|
||||||
import com.facebook.imagepipeline.image.ImageInfo
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.android.closestDI
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import org.kodein.di.on
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
|
||||||
import xyz.quaver.pupil.sources.Hitomi
|
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
class SearchResultsAdapter(var results: LiveData<List<ItemInfo>>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
|
||||||
|
|
||||||
var onChipClickedHandler: ((Tag) -> Unit)? = null
|
|
||||||
var onDownloadClickedHandler: ((source: String, itemI: String) -> Unit)? = null
|
|
||||||
var onDeleteClickedHandler: ((source: String, itemID: String) -> Unit)? = null
|
|
||||||
|
|
||||||
inner class ViewHolder(private val binding: SearchResultItemBinding) : RecyclerView.ViewHolder(binding.root), DIAware {
|
|
||||||
override val di by closestDI(binding.root.context)
|
|
||||||
|
|
||||||
private val clipboardManager: ClipboardManager by di.on(itemView.context).instance()
|
|
||||||
|
|
||||||
var source: String = ""
|
|
||||||
var itemID: String = ""
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.root.binding.download.setOnClickListener {
|
|
||||||
onDownloadClickedHandler?.invoke(source, itemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.binding.delete.setOnClickListener {
|
|
||||||
onDeleteClickedHandler?.invoke(source, itemID)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.idView.setOnClickListener {
|
|
||||||
clipboardManager.setPrimaryClip(
|
|
||||||
ClipData.newPlainText("item_id", itemID)
|
|
||||||
)
|
|
||||||
Toast.makeText(itemView.context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
|
|
||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
|
||||||
mItemManger.closeAllExcept(layout)
|
|
||||||
|
|
||||||
binding.root.binding.download.text = itemView.context.getString(R.string.main_download)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOpen(layout: SwipeLayout?) {}
|
|
||||||
override fun onStartClose(layout: SwipeLayout?) {}
|
|
||||||
override fun onClose(layout: SwipeLayout?) {}
|
|
||||||
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
|
|
||||||
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.tagGroup.onClickListener = onChipClickedHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
private val controllerListener = object: BaseControllerListener<ImageInfo>() {
|
|
||||||
override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
|
|
||||||
imageInfo?.let {
|
|
||||||
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
|
|
||||||
imageInfo?.let {
|
|
||||||
binding.thumbnail.aspectRatio = it.width / it.height.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun bind(result: ItemInfo) {
|
|
||||||
source = result.source
|
|
||||||
itemID = result.id
|
|
||||||
|
|
||||||
binding.root.progress = 0
|
|
||||||
|
|
||||||
binding.thumbnail.controller = Fresco.newDraweeControllerBuilder()
|
|
||||||
.setUri(result.thumbnail)
|
|
||||||
.setOldController(binding.thumbnail.controller)
|
|
||||||
.setControllerListener(controllerListener)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
binding.title.text = result.title
|
|
||||||
binding.idView.text = result.id
|
|
||||||
|
|
||||||
binding.artist.visibility = if (result.artists.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
binding.artist.text = result.artists
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
with (binding.tagGroup) {
|
|
||||||
tags.clear()
|
|
||||||
source = result.source
|
|
||||||
result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.let { if (it.size == 1 && it.first().isEmpty()) emptyList() else it }?.map {
|
|
||||||
Tag.parse(it)
|
|
||||||
}?.let { tags.addAll(it) }
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val extraType = listOf(
|
|
||||||
ItemInfo.ExtraType.SERIES,
|
|
||||||
ItemInfo.ExtraType.TYPE,
|
|
||||||
ItemInfo.ExtraType.LANGUAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
result.extra[ItemInfo.ExtraType.GROUP]?.await()?.let {
|
|
||||||
if (it.isNotEmpty())
|
|
||||||
binding.artist.text = "${result.artists} ($it)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
binding.extra.text =
|
|
||||||
result.extra.entries.filter { it.key in extraType && it.value.await() != null }.fold(StringBuilder()) { res, entry ->
|
|
||||||
entry.value.await()?.let {
|
|
||||||
if (it.isNotEmpty()) {
|
|
||||||
res.append(
|
|
||||||
itemView.context.getString(
|
|
||||||
ItemInfo.extraTypeMap[entry.key] ?: error(""),
|
|
||||||
if (entry.key == ItemInfo.ExtraType.LANGUAGE) Hitomi.languageMap[entry.value.await()] else entry.value.await()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
res.append('\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
binding.pagecount.text = result.extra[ItemInfo.ExtraType.PAGECOUNT]?.let {
|
|
||||||
itemView.context.getString(
|
|
||||||
ItemInfo.extraTypeMap[ItemInfo.ExtraType.PAGECOUNT] ?: error(""),
|
|
||||||
it.await()
|
|
||||||
)
|
|
||||||
} ?: "-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
|
||||||
ViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
mItemManger.bindView(holder.itemView, position)
|
|
||||||
holder.bind(results.value!![position])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = results.value?.size ?: 0
|
|
||||||
|
|
||||||
override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
|
import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
import xyz.quaver.pupil.sources.SourceEntries
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
|
|
||||||
|
|||||||
31
app/src/main/java/xyz/quaver/pupil/db/Bookmark.kt
Normal file
31
app/src/main/java/xyz/quaver/pupil/db/Bookmark.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package xyz.quaver.pupil.db
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["source", "itemID"])
|
||||||
|
data class Bookmark(
|
||||||
|
val source: String,
|
||||||
|
val itemID: String,
|
||||||
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface BookmarkDao {
|
||||||
|
@Query("SELECT * FROM bookmark")
|
||||||
|
fun getAll(): LiveData<List<Bookmark>>
|
||||||
|
|
||||||
|
@Query("SELECT itemID FROM bookmark WHERE source = :source")
|
||||||
|
fun getAll(source: String): LiveData<List<String>>
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT * FROM bookmark WHERE source = :source AND itemID = :itemID)")
|
||||||
|
fun contains(source: String, itemID: String): LiveData<Boolean>
|
||||||
|
|
||||||
|
fun contains(bookmark: Bookmark) = contains(bookmark.source, bookmark.itemID)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(bookmark: Bookmark)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(bookmark: Bookmark)
|
||||||
|
}
|
||||||
17
app/src/main/java/xyz/quaver/pupil/db/Database.kt
Normal file
17
app/src/main/java/xyz/quaver/pupil/db/Database.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package xyz.quaver.pupil.db
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import org.kodein.di.*
|
||||||
|
|
||||||
|
@Database(entities = [History::class, Bookmark::class], version = 1)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun historyDao(): HistoryDao
|
||||||
|
abstract fun bookmarkDao(): BookmarkDao
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseModule = DI.Module("database") {
|
||||||
|
bind<AppDatabase>() with singleton { Room.databaseBuilder(instance<Application>(), AppDatabase::class.java, "pupil").build() }
|
||||||
|
}
|
||||||
26
app/src/main/java/xyz/quaver/pupil/db/History.kt
Normal file
26
app/src/main/java/xyz/quaver/pupil/db/History.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package xyz.quaver.pupil.db
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["source", "itemID"])
|
||||||
|
data class History(
|
||||||
|
val source: String,
|
||||||
|
val itemID: String,
|
||||||
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface HistoryDao {
|
||||||
|
@Query("SELECT * FROM history")
|
||||||
|
fun getAll(): LiveData<List<History>>
|
||||||
|
|
||||||
|
@Query("SELECT itemID FROM history WHERE source = :source")
|
||||||
|
fun getAll(source: String): LiveData<List<String>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(history: History)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(history: History)
|
||||||
|
}
|
||||||
@@ -18,101 +18,36 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||||
|
|
||||||
@Serializable(with = ItemInfo.SearchResultSerializer::class)
|
interface ItemInfo : Parcelable {
|
||||||
data class ItemInfo(
|
val source: String
|
||||||
val source: String,
|
val itemID: String
|
||||||
val id: String,
|
val title: String
|
||||||
val title: String,
|
|
||||||
val thumbnail: String,
|
|
||||||
val artists: String,
|
|
||||||
val extra: Map<ExtraType, Deferred<String?>> = emptyMap()
|
|
||||||
) {
|
|
||||||
enum class ExtraType {
|
|
||||||
GROUP,
|
|
||||||
CHARACTER,
|
|
||||||
SERIES,
|
|
||||||
TYPE,
|
|
||||||
TAGS,
|
|
||||||
LANGUAGE,
|
|
||||||
PAGECOUNT,
|
|
||||||
PREVIEW,
|
|
||||||
RELATED_ITEM,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@SerialName("SearchResult")
|
|
||||||
data class ItemInfoSurrogate(
|
|
||||||
val source: String,
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val thumbnail: String,
|
|
||||||
val artists: String,
|
|
||||||
val extra: Map<ExtraType, String?> = emptyMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
object SearchResultSerializer : KSerializer<ItemInfo> {
|
|
||||||
override val descriptor = ItemInfoSurrogate.serializer().descriptor
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: ItemInfo) {
|
|
||||||
val surrogate = ItemInfoSurrogate(
|
|
||||||
value.source,
|
|
||||||
value.id,
|
|
||||||
value.title,
|
|
||||||
value.thumbnail,
|
|
||||||
value.artists,
|
|
||||||
value.extra.mapValues { runBlocking { it.value.await() } }
|
|
||||||
)
|
|
||||||
encoder.encodeSerializableValue(ItemInfoSurrogate.serializer(), surrogate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): ItemInfo {
|
|
||||||
val surrogate = decoder.decodeSerializableValue(ItemInfoSurrogate.serializer())
|
|
||||||
return ItemInfo(
|
|
||||||
surrogate.source,
|
|
||||||
surrogate.id,
|
|
||||||
surrogate.title,
|
|
||||||
surrogate.thumbnail,
|
|
||||||
surrogate.artists,
|
|
||||||
surrogate.extra.mapValues { CoroutineScope(Dispatchers.Unconfined).async { it.value } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isReady: Boolean
|
|
||||||
get() = extra.all { it.value.isCompleted }
|
|
||||||
|
|
||||||
suspend fun awaitAll() = extra.values.awaitAll()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val extraTypeMap = mapOf(
|
|
||||||
ExtraType.SERIES to R.string.galleryblock_series,
|
|
||||||
ExtraType.TYPE to R.string.galleryblock_type,
|
|
||||||
ExtraType.LANGUAGE to R.string.galleryblock_language,
|
|
||||||
ExtraType.PAGECOUNT to R.string.galleryblock_pagecount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
|
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
|
||||||
|
|
||||||
interface SortModeInterface {
|
data class SearchResultEvent(val type: Type, val payload: String) {
|
||||||
val ordinal: Int
|
enum class Type {
|
||||||
val name: Int
|
OPEN_READER,
|
||||||
|
OPEN_DETAILS,
|
||||||
|
NEW_QUERY,
|
||||||
|
TOGGLE_FAVORITES
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Source {
|
abstract class Source {
|
||||||
@@ -121,10 +56,13 @@ abstract class Source {
|
|||||||
abstract val preferenceID: Int
|
abstract val preferenceID: Int
|
||||||
abstract val availableSortMode: List<String>
|
abstract val availableSortMode: List<String>
|
||||||
|
|
||||||
abstract suspend fun search(query: String, range: IntRange, sortMode: Int) : Pair<Channel<ItemInfo>, Int>
|
abstract suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int>
|
||||||
abstract suspend fun suggestion(query: String) : List<SearchSuggestion>
|
abstract suspend fun suggestion(query: String): List<SearchSuggestion>
|
||||||
abstract suspend fun images(itemID: String) : List<String>
|
abstract suspend fun images(itemID: String): List<String>
|
||||||
abstract suspend fun info(itemID: String) : ItemInfo
|
abstract suspend fun info(itemID: String): ItemInfo
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
open fun SearchResult(itemInfo: ItemInfo, onEvent: ((SearchResultEvent) -> Unit)? = null) { }
|
||||||
|
|
||||||
open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { }
|
open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { }
|
||||||
|
|
||||||
@@ -135,22 +73,15 @@ abstract class Source {
|
|||||||
|
|
||||||
typealias SourceEntry = Pair<String, Source>
|
typealias SourceEntry = Pair<String, Source>
|
||||||
typealias SourceEntries = Set<SourceEntry>
|
typealias SourceEntries = Set<SourceEntry>
|
||||||
typealias SourcePreferenceID = Pair<String, Int>
|
|
||||||
typealias SourcePreferenceIDs = Set<SourcePreferenceID>
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val sourceModule = DI.Module(name = "source") {
|
val sourceModule = DI.Module(name = "source") {
|
||||||
bindSet<SourceEntry>()
|
bindSet<SourceEntry>()
|
||||||
bindSet<SourcePreferenceID>()
|
|
||||||
|
|
||||||
onReady {
|
listOf<(Application) -> (Source)>(
|
||||||
listOf<Source>(
|
{ Hitomi(it) }
|
||||||
Hitomi(instance())
|
).forEach { source ->
|
||||||
).forEach { source ->
|
inSet { singleton { source.invoke(instance()).let { it.name to it } } }
|
||||||
inSet { multiton { _: Unit -> source.name to source } }
|
|
||||||
inSet { singleton { source.name to source.preferenceID } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bind { factory { source: String -> History(di, source) } }
|
bind { singleton { History(di) } }
|
||||||
inSet { singleton { Downloads(di).let { it.name to it as Source } } }
|
// inSet { singleton { Downloads(di).let { it.name to it as Source } } }
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@@ -32,7 +33,7 @@ import xyz.quaver.pupil.R
|
|||||||
import xyz.quaver.pupil.util.DownloadManager
|
import xyz.quaver.pupil.util.DownloadManager
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
/*
|
||||||
class Downloads(override val di: DI) : Source(), DIAware {
|
class Downloads(override val di: DI) : Source(), DIAware {
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
@@ -46,6 +47,8 @@ class Downloads(override val di: DI) : Source(), DIAware {
|
|||||||
private val downloadManager: DownloadManager by instance()
|
private val downloadManager: DownloadManager by instance()
|
||||||
|
|
||||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
||||||
|
TODO()
|
||||||
|
/*
|
||||||
val downloads = downloadManager.downloads.toList()
|
val downloads = downloadManager.downloads.toList()
|
||||||
|
|
||||||
val channel = Channel<ItemInfo>()
|
val channel = Channel<ItemInfo>()
|
||||||
@@ -61,7 +64,7 @@ class Downloads(override val di: DI) : Source(), DIAware {
|
|||||||
channel.close()
|
channel.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(channel, downloads.size)
|
return Pair(channel, downloads.size)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
||||||
@@ -75,7 +78,8 @@ class Downloads(override val di: DI) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun info(itemID: String): ItemInfo {
|
override suspend fun info(itemID: String): ItemInfo {
|
||||||
return transform(downloadManager.downloadFolder.getChild(itemID))
|
TODO("Not yet implemented")
|
||||||
|
/* return transform(downloadManager.downloadFolder.getChild(itemID))*/
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -83,7 +87,7 @@ class Downloads(override val di: DI) : Source(), DIAware {
|
|||||||
folder.list { _, name ->
|
folder.list { _, name ->
|
||||||
name.takeLastWhile { it != '.' } in listOf("jpg", "png", "gif", "webp")
|
name.takeLastWhile { it != '.' } in listOf("jpg", "png", "gif", "webp")
|
||||||
}?.toList()
|
}?.toList()
|
||||||
|
/*
|
||||||
suspend fun transform(folder: FileX): ItemInfo = withContext(Dispatchers.Unconfined) {
|
suspend fun transform(folder: FileX): ItemInfo = withContext(Dispatchers.Unconfined) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
Json.decodeFromString<ItemInfo>(folder.getChild(".metadata").readText())
|
Json.decodeFromString<ItemInfo>(folder.getChild(".metadata").readText())
|
||||||
@@ -100,7 +104,12 @@ class Downloads(override val di: DI) : Source(), DIAware {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Composable
|
||||||
|
override fun compose(itemInfo: ItemInfo) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}*/
|
||||||
@@ -18,55 +18,63 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.sources
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.direct
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.util.SavedSourceSet
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
|
import xyz.quaver.pupil.util.database
|
||||||
import xyz.quaver.pupil.util.source
|
import xyz.quaver.pupil.util.source
|
||||||
|
|
||||||
class History(override val di: DI, source: String) : Source(), DIAware {
|
class History(override val di: DI) : Source(), DIAware {
|
||||||
|
|
||||||
private val source: Source by source(source)
|
private val historyDao = direct.database().historyDao()
|
||||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
get() = source.name
|
get() = "history"
|
||||||
override val iconResID: Int
|
override val iconResID: Int
|
||||||
get() = source.iconResID
|
get() = 0 //TODO
|
||||||
override val preferenceID: Int
|
override val preferenceID: Int
|
||||||
get() = source.preferenceID
|
get() = 0 //TODO
|
||||||
override val availableSortMode: List<String> = emptyList()
|
override val availableSortMode: List<String> = emptyList()
|
||||||
|
|
||||||
|
private val history = direct.database().historyDao()
|
||||||
|
|
||||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
||||||
val channel = Channel<ItemInfo>()
|
val channel = Channel<ItemInfo>()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
histories[source.name]?.asReversed()?.forEach {
|
|
||||||
channel.send(source.info(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.close()
|
channel.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(channel, histories.map.size)
|
throw NotImplementedError("")
|
||||||
|
//return Pair(channel, histories.map.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
||||||
return source.suggestion(query)
|
throw NotImplementedError("")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(itemID: String): List<String> {
|
override suspend fun images(itemID: String): List<String> {
|
||||||
return source.images(itemID)
|
throw NotImplementedError("")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun info(itemID: String): ItemInfo {
|
override suspend fun info(itemID: String): ItemInfo {
|
||||||
return source.info(itemID)
|
throw NotImplementedError("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun SearchResult(itemInfo: ItemInfo, onEvent: ((SearchResultEvent) -> Unit)?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.sources
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.TextView
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
|
||||||
import xyz.quaver.hitomi.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.sources.ItemInfo.ExtraType
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class Hitomi(app: Application) : Source() {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
|
||||||
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val body = s
|
|
||||||
/*
|
|
||||||
TODO
|
|
||||||
if (translations[s] != null)
|
|
||||||
"${translations[s]} ($s)"
|
|
||||||
else
|
|
||||||
s
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name: String = "hitomi.la"
|
|
||||||
override val iconResID: Int = R.drawable.hitomi
|
|
||||||
override val preferenceID: Int = R.xml.hitomi_preferences
|
|
||||||
override val availableSortMode: List<String> = app.resources.getStringArray(R.array.hitomi_sort_mode).toList()
|
|
||||||
|
|
||||||
var cachedQuery: String? = null
|
|
||||||
var cachedSortMode: Int = -1
|
|
||||||
val cache = mutableListOf<Int>()
|
|
||||||
|
|
||||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
|
||||||
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
|
||||||
cachedQuery = null
|
|
||||||
cache.clear()
|
|
||||||
yield()
|
|
||||||
doSearch("$query ${Preferences["hitomi.default_query", ""]}", sortMode == 1).let {
|
|
||||||
yield()
|
|
||||||
cache.addAll(it)
|
|
||||||
}
|
|
||||||
cachedQuery = query
|
|
||||||
}
|
|
||||||
|
|
||||||
val channel = Channel<ItemInfo>()
|
|
||||||
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
cache.slice(sanitizedRange).map {
|
|
||||||
async {
|
|
||||||
getGalleryBlock(it)
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
channel.send(transform(name, it.await()))
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(channel, cache.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
|
||||||
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
|
||||||
TagSuggestion(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun images(itemID: String): List<String> {
|
|
||||||
val galleryID = itemID.toInt()
|
|
||||||
|
|
||||||
val reader = getGalleryInfo(galleryID)
|
|
||||||
|
|
||||||
return reader.files.map {
|
|
||||||
imageUrlFromImage(galleryID, it, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun info(itemID: String): ItemInfo = coroutineScope {
|
|
||||||
kotlin.runCatching {
|
|
||||||
getGallery(itemID.toInt()).let {
|
|
||||||
ItemInfo(
|
|
||||||
name,
|
|
||||||
itemID,
|
|
||||||
it.title,
|
|
||||||
it.cover,
|
|
||||||
it.artists.joinToString { it.wordCapitalize() },
|
|
||||||
mapOf(
|
|
||||||
ExtraType.TYPE to async { it.type.wordCapitalize() },
|
|
||||||
ExtraType.GROUP to async { it.groups.joinToString { it.wordCapitalize() } },
|
|
||||||
ExtraType.LANGUAGE to async { it.language },
|
|
||||||
ExtraType.SERIES to async { it.series.joinToString { it.wordCapitalize() } },
|
|
||||||
ExtraType.CHARACTER to async { it.characters.joinToString { it.wordCapitalize() } },
|
|
||||||
ExtraType.TAGS to async { it.tags.joinToString() },
|
|
||||||
ExtraType.PREVIEW to async { it.thumbnails.joinToString() },
|
|
||||||
ExtraType.RELATED_ITEM to async { it.related.joinToString() },
|
|
||||||
ExtraType.PAGECOUNT to async { it.thumbnails.size.toString() },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
|
||||||
transform(name, getGalleryBlock(itemID.toInt()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = {
|
|
||||||
append("Referer", getReferer(itemID.toInt()))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {
|
|
||||||
item as TagSuggestion
|
|
||||||
|
|
||||||
binding.leftIcon.setImageResource(
|
|
||||||
when(item.n) {
|
|
||||||
"female" -> R.drawable.gender_female
|
|
||||||
"male" -> R.drawable.gender_male
|
|
||||||
"language" -> R.drawable.translate
|
|
||||||
"group" -> R.drawable.account_group
|
|
||||||
"character" -> R.drawable.account_star
|
|
||||||
"series" -> R.drawable.book_open
|
|
||||||
"artist" -> R.drawable.brush
|
|
||||||
else -> R.drawable.tag
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (item.t > 0) {
|
|
||||||
with (binding.root) {
|
|
||||||
val count = findViewById<TextView>(R.id.count)
|
|
||||||
if (count == null)
|
|
||||||
addView(
|
|
||||||
LayoutInflater.from(context).inflate(R.layout.suggestion_count, binding.root, false)
|
|
||||||
.apply {
|
|
||||||
this as TextView
|
|
||||||
|
|
||||||
text = item.t.toString()
|
|
||||||
}, 2
|
|
||||||
)
|
|
||||||
else
|
|
||||||
count.text = item.t.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val languageMap = mapOf(
|
|
||||||
"indonesian" to "Bahasa Indonesia",
|
|
||||||
"catalan" to "català",
|
|
||||||
"cebuano" to "Cebuano",
|
|
||||||
"czech" to "Čeština",
|
|
||||||
"danish" to "Dansk",
|
|
||||||
"german" to "Deutsch",
|
|
||||||
"estonian" to "eesti",
|
|
||||||
"english" to "English",
|
|
||||||
"spanish" to "Español",
|
|
||||||
"esperanto" to "Esperanto",
|
|
||||||
"french" to "Français",
|
|
||||||
"italian" to "Italiano",
|
|
||||||
"latin" to "Latina",
|
|
||||||
"hungarian" to "magyar",
|
|
||||||
"dutch" to "Nederlands",
|
|
||||||
"norwegian" to "norsk",
|
|
||||||
"polish" to "polski",
|
|
||||||
"portuguese" to "Português",
|
|
||||||
"romanian" to "română",
|
|
||||||
"albanian" to "shqip",
|
|
||||||
"slovak" to "Slovenčina",
|
|
||||||
"finnish" to "Suomi",
|
|
||||||
"swedish" to "Svenska",
|
|
||||||
"tagalog" to "Tagalog",
|
|
||||||
"vietnamese" to "tiếng việt",
|
|
||||||
"turkish" to "Türkçe",
|
|
||||||
"greek" to "Ελληνικά",
|
|
||||||
"mongolian" to "Монгол",
|
|
||||||
"russian" to "Русский",
|
|
||||||
"ukrainian" to "Українська",
|
|
||||||
"hebrew" to "עברית",
|
|
||||||
"arabic" to "العربية",
|
|
||||||
"persian" to "فارسی",
|
|
||||||
"thai" to "ไทย",
|
|
||||||
"korean" to "한국어",
|
|
||||||
"chinese" to "中文",
|
|
||||||
"japanese" to "日本語"
|
|
||||||
)
|
|
||||||
|
|
||||||
fun transform(name: String, galleryBlock: GalleryBlock) =
|
|
||||||
ItemInfo(
|
|
||||||
name,
|
|
||||||
galleryBlock.id.toString(),
|
|
||||||
galleryBlock.title,
|
|
||||||
galleryBlock.thumbnails.first(),
|
|
||||||
galleryBlock.artists.joinToString { it.wordCapitalize() },
|
|
||||||
mapOf(
|
|
||||||
ExtraType.GROUP to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
|
|
||||||
getGallery(galleryBlock.id).groups.joinToString { it.wordCapitalize() }
|
|
||||||
}.getOrDefault("") },
|
|
||||||
ExtraType.SERIES to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.series.joinToString { it.wordCapitalize() } },
|
|
||||||
ExtraType.TYPE to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.type.wordCapitalize() },
|
|
||||||
ExtraType.LANGUAGE to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.language },
|
|
||||||
ExtraType.PAGECOUNT to CoroutineScope(Dispatchers.IO).async { kotlin.runCatching {
|
|
||||||
getGalleryInfo(galleryBlock.id).files.size.toString()
|
|
||||||
}.getOrNull() },
|
|
||||||
ExtraType.TAGS to CoroutineScope(Dispatchers.Unconfined).async { galleryBlock.relatedTags.joinToString() }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,7 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
/*
|
||||||
class ImHentai(override val di: DI) : Source(), DIAware {
|
class ImHentai(override val di: DI) : Source(), DIAware {
|
||||||
|
|
||||||
private val app: Application by instance()
|
private val app: Application by instance()
|
||||||
@@ -85,4 +85,4 @@ class ImHentai(override val di: DI) : Source(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}*/
|
||||||
561
app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
Normal file
561
app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
/*
|
||||||
|
* 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.sources.hitomi
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Female
|
||||||
|
import androidx.compose.material.icons.filled.Male
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.outlined.StarOutline
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
import coil.annotation.ExperimentalCoilApi
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.closestDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||||
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
|
import xyz.quaver.hitomi.*
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
|
import xyz.quaver.pupil.db.Bookmark
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import xyz.quaver.pupil.sources.SearchResultEvent
|
||||||
|
import xyz.quaver.pupil.sources.Source
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Parcelize
|
||||||
|
data class HitomiItemInfo(
|
||||||
|
override val itemID: String,
|
||||||
|
override val title: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val artists: List<String>,
|
||||||
|
val series: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val language: String,
|
||||||
|
val tags: List<String>,
|
||||||
|
private var groups: List<String>? = null,
|
||||||
|
private var pageCount: Int? = null,
|
||||||
|
val characters: List<String>? = null,
|
||||||
|
val preview: List<String>? = null,
|
||||||
|
val relatedItem: List<String>? = null
|
||||||
|
): ItemInfo {
|
||||||
|
|
||||||
|
override val source: String
|
||||||
|
get() = "hitomi.la"
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
private val groupMutex = Mutex()
|
||||||
|
suspend fun getGroups() = withContext(Dispatchers.IO) {
|
||||||
|
if (groups != null) groups
|
||||||
|
else groupMutex.withLock { runCatching {
|
||||||
|
getGallery(itemID.toInt()).groups
|
||||||
|
}.getOrNull() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
private val pageCountMutex = Mutex()
|
||||||
|
suspend fun getPageCount() = withContext(Dispatchers.IO) {
|
||||||
|
if (pageCount != null) pageCount
|
||||||
|
|
||||||
|
else pageCountMutex.withLock { runCatching {
|
||||||
|
getGalleryInfo(itemID.toInt()).files.size.also { pageCount = it }
|
||||||
|
}.getOrNull() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hitomi(app: Application) : Source(), DIAware {
|
||||||
|
|
||||||
|
override val di: DI by closestDI(app)
|
||||||
|
|
||||||
|
private val database: AppDatabase by instance()
|
||||||
|
|
||||||
|
private val bookmarkDao = database.bookmarkDao()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
||||||
|
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
override val body = s
|
||||||
|
/*
|
||||||
|
TODO
|
||||||
|
if (translations[s] != null)
|
||||||
|
"${translations[s]} ($s)"
|
||||||
|
else
|
||||||
|
s
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name: String = "hitomi.la"
|
||||||
|
override val iconResID: Int = R.drawable.hitomi
|
||||||
|
override val preferenceID: Int = R.xml.hitomi_preferences
|
||||||
|
override val availableSortMode: List<String> = app.resources.getStringArray(R.array.hitomi_sort_mode).toList()
|
||||||
|
|
||||||
|
var cachedQuery: String? = null
|
||||||
|
var cachedSortMode: Int = -1
|
||||||
|
private val cache = mutableListOf<Int>()
|
||||||
|
|
||||||
|
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
||||||
|
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
||||||
|
cachedQuery = null
|
||||||
|
cache.clear()
|
||||||
|
yield()
|
||||||
|
doSearch("$query ${Preferences["hitomi.default_query", ""]}", sortMode == 1).let {
|
||||||
|
yield()
|
||||||
|
cache.addAll(it)
|
||||||
|
}
|
||||||
|
cachedQuery = query
|
||||||
|
}
|
||||||
|
|
||||||
|
val channel = Channel<ItemInfo>()
|
||||||
|
val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
cache.slice(sanitizedRange).map {
|
||||||
|
async {
|
||||||
|
getGalleryBlock(it)
|
||||||
|
}
|
||||||
|
}.forEach {
|
||||||
|
channel.send(transform(it.await()))
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(channel, cache.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
||||||
|
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
||||||
|
TagSuggestion(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun images(itemID: String): List<String> {
|
||||||
|
val galleryID = itemID.toInt()
|
||||||
|
|
||||||
|
val reader = getGalleryInfo(galleryID)
|
||||||
|
|
||||||
|
return reader.files.map {
|
||||||
|
imageUrlFromImage(galleryID, it, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun info(itemID: String): HitomiItemInfo = withContext(Dispatchers.IO) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
getGallery(itemID.toInt()).let {
|
||||||
|
HitomiItemInfo(
|
||||||
|
itemID,
|
||||||
|
it.title,
|
||||||
|
it.cover,
|
||||||
|
it.artists,
|
||||||
|
it.series,
|
||||||
|
it.type,
|
||||||
|
it.language,
|
||||||
|
it.tags,
|
||||||
|
it.groups,
|
||||||
|
it.thumbnails.size,
|
||||||
|
it.characters,
|
||||||
|
it.thumbnails,
|
||||||
|
it.related.map { it.toString() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
transform(getGalleryBlock(itemID.toInt()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun SearchResult(itemInfo: ItemInfo, onEvent: ((SearchResultEvent) -> Unit)?) {
|
||||||
|
itemInfo as HitomiItemInfo
|
||||||
|
|
||||||
|
FullSearchResult(itemInfo = itemInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = {
|
||||||
|
append("Referer", getReferer(itemID.toInt()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {
|
||||||
|
item as TagSuggestion
|
||||||
|
|
||||||
|
binding.leftIcon.setImageResource(
|
||||||
|
when(item.n) {
|
||||||
|
"female" -> R.drawable.gender_female
|
||||||
|
"male" -> R.drawable.gender_male
|
||||||
|
"language" -> R.drawable.translate
|
||||||
|
"group" -> R.drawable.account_group
|
||||||
|
"character" -> R.drawable.account_star
|
||||||
|
"series" -> R.drawable.book_open
|
||||||
|
"artist" -> R.drawable.brush
|
||||||
|
else -> R.drawable.tag
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (item.t > 0) {
|
||||||
|
with (binding.root) {
|
||||||
|
val count = findViewById<TextView>(R.id.count)
|
||||||
|
if (count == null)
|
||||||
|
addView(
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.suggestion_count, binding.root, false)
|
||||||
|
.apply {
|
||||||
|
this as TextView
|
||||||
|
|
||||||
|
text = item.t.toString()
|
||||||
|
}, 2
|
||||||
|
)
|
||||||
|
else
|
||||||
|
count.text = item.t.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val languageMap = mapOf(
|
||||||
|
"indonesian" to "Bahasa Indonesia",
|
||||||
|
"catalan" to "català",
|
||||||
|
"cebuano" to "Cebuano",
|
||||||
|
"czech" to "Čeština",
|
||||||
|
"danish" to "Dansk",
|
||||||
|
"german" to "Deutsch",
|
||||||
|
"estonian" to "eesti",
|
||||||
|
"english" to "English",
|
||||||
|
"spanish" to "Español",
|
||||||
|
"esperanto" to "Esperanto",
|
||||||
|
"french" to "Français",
|
||||||
|
"italian" to "Italiano",
|
||||||
|
"latin" to "Latina",
|
||||||
|
"hungarian" to "magyar",
|
||||||
|
"dutch" to "Nederlands",
|
||||||
|
"norwegian" to "norsk",
|
||||||
|
"polish" to "polski",
|
||||||
|
"portuguese" to "Português",
|
||||||
|
"romanian" to "română",
|
||||||
|
"albanian" to "shqip",
|
||||||
|
"slovak" to "Slovenčina",
|
||||||
|
"finnish" to "Suomi",
|
||||||
|
"swedish" to "Svenska",
|
||||||
|
"tagalog" to "Tagalog",
|
||||||
|
"vietnamese" to "tiếng việt",
|
||||||
|
"turkish" to "Türkçe",
|
||||||
|
"greek" to "Ελληνικά",
|
||||||
|
"mongolian" to "Монгол",
|
||||||
|
"russian" to "Русский",
|
||||||
|
"ukrainian" to "Українська",
|
||||||
|
"hebrew" to "עברית",
|
||||||
|
"arabic" to "العربية",
|
||||||
|
"persian" to "فارسی",
|
||||||
|
"thai" to "ไทย",
|
||||||
|
"korean" to "한국어",
|
||||||
|
"chinese" to "中文",
|
||||||
|
"japanese" to "日本語"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun transform(galleryBlock: GalleryBlock) =
|
||||||
|
HitomiItemInfo(
|
||||||
|
galleryBlock.id.toString(),
|
||||||
|
galleryBlock.title,
|
||||||
|
galleryBlock.thumbnails.first(),
|
||||||
|
galleryBlock.artists,
|
||||||
|
galleryBlock.series,
|
||||||
|
galleryBlock.type,
|
||||||
|
galleryBlock.language,
|
||||||
|
galleryBlock.relatedTags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TagChip(tag: String, isFavorite: Boolean, onClick: ((String) -> Unit)? = null, onFavoriteClick: ((String) -> Unit)? = null) {
|
||||||
|
val tagParts = tag.split(":", limit = 2).let {
|
||||||
|
if (it.size == 1) listOf("", it.first())
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon = when (tagParts[0]) {
|
||||||
|
"male" -> Icons.Filled.Male
|
||||||
|
"female" -> Icons.Filled.Female
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val (surfaceColor, textTint) = when {
|
||||||
|
isFavorite -> Pair(colorResource(id = R.color.material_orange_500), Color.White)
|
||||||
|
else -> when (tagParts[0]) {
|
||||||
|
"male" -> Pair(colorResource(id = R.color.material_blue_700), Color.White)
|
||||||
|
"female" -> Pair(colorResource(id = R.color.material_pink_600), Color.White)
|
||||||
|
else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
onClick = { onClick?.invoke(tag) },
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = surfaceColor,
|
||||||
|
elevation = 2.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (icon != null)
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = "Icon",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Box(Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
tagParts[1],
|
||||||
|
color = textTint,
|
||||||
|
style = MaterialTheme.typography.body2
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
starIcon,
|
||||||
|
contentDescription = "Favorites",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { onFavoriteClick?.invoke(tag) },
|
||||||
|
tint = textTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TagGroup(tags: List<String>) {
|
||||||
|
var isFolded by remember { mutableStateOf(true) }
|
||||||
|
val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
|
||||||
|
|
||||||
|
val bookmarkedTagsInList = bookmarkedTags.toSet() intersect tags
|
||||||
|
|
||||||
|
FlowRow(Modifier.padding(0.dp, 16.dp)) {
|
||||||
|
tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
|
||||||
|
TagChip(
|
||||||
|
tag = tag,
|
||||||
|
isFavorite = bookmarkedTagsInList.contains(tag),
|
||||||
|
onFavoriteClick = { tag ->
|
||||||
|
val bookmarkTag = Bookmark(name, tag)
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (bookmarkedTagsInList.contains(tag))
|
||||||
|
bookmarkDao.delete(bookmarkTag)
|
||||||
|
else
|
||||||
|
bookmarkDao.insert(bookmarkTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolded && tags.size > 10)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
color = MaterialTheme.colors.background,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = 2.dp,
|
||||||
|
onClick = { isFolded = false }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"…",
|
||||||
|
modifier = Modifier.padding(16.dp, 8.dp),
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.body2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FullSearchResult(itemInfo: HitomiItemInfo) {
|
||||||
|
var group by remember { mutableStateOf(emptyList<String>()) }
|
||||||
|
var pageCount by remember { mutableStateOf("-") }
|
||||||
|
|
||||||
|
LaunchedEffect(itemInfo) {
|
||||||
|
launch {
|
||||||
|
itemInfo.getPageCount()?.run {
|
||||||
|
pageCount = "${this}P"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
itemInfo.getGroups()?.run {
|
||||||
|
group = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val painter = rememberImagePainter(itemInfo.thumbnail)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row {
|
||||||
|
Image(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(150.dp)
|
||||||
|
.aspectRatio(
|
||||||
|
with(painter.intrinsicSize) { if (this == Size.Companion.Unspecified) 1f else width / height },
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.padding(0.dp, 0.dp, 8.dp, 0.dp)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
itemInfo.title,
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
val artistStringBuilder = StringBuilder()
|
||||||
|
|
||||||
|
with (itemInfo.artists) {
|
||||||
|
if (this.isNotEmpty())
|
||||||
|
artistStringBuilder.append(this.joinToString(", ") { it.wordCapitalize() })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.isNotEmpty()) {
|
||||||
|
if (artistStringBuilder.isNotEmpty()) artistStringBuilder.append(" ")
|
||||||
|
|
||||||
|
artistStringBuilder.append("(")
|
||||||
|
artistStringBuilder.append(group.joinToString(", ") { it.wordCapitalize() })
|
||||||
|
artistStringBuilder.append(")")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistStringBuilder.isNotEmpty())
|
||||||
|
Text(
|
||||||
|
artistStringBuilder.toString(),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (itemInfo.series.isNotEmpty())
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
id = R.string.galleryblock_series,
|
||||||
|
itemInfo.series.joinToString { it.wordCapitalize() }
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.galleryblock_type, itemInfo.type),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
languageMap[itemInfo.language]?.run {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.galleryblock_language, this),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TagGroup(tags = itemInfo.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier.padding(0.dp, 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
itemInfo.itemID,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
pageCount,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painterResource(id = R.drawable.ic_star_empty),
|
||||||
|
contentDescription = "Favorite",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.padding(4.dp)
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,46 +18,51 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
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.text.InputType
|
import android.text.InputType
|
||||||
import android.text.util.Linkify
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.orhanobut.logger.Logger
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||||
import xyz.quaver.pupil.*
|
import xyz.quaver.pupil.*
|
||||||
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import xyz.quaver.pupil.sources.Source
|
||||||
import xyz.quaver.pupil.types.*
|
import xyz.quaver.pupil.types.*
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||||
import xyz.quaver.pupil.ui.dialog.GalleryDialogFragment
|
|
||||||
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
|
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
|
||||||
import xyz.quaver.pupil.ui.view.ProgressCardView
|
import xyz.quaver.pupil.ui.view.ProgressCardView
|
||||||
import xyz.quaver.pupil.ui.view.SwipePageTurnView
|
|
||||||
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.util.regex.Pattern
|
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
class MainActivity :
|
class MainActivity :
|
||||||
@@ -70,11 +75,63 @@ class MainActivity :
|
|||||||
private lateinit var binding: MainActivityBinding
|
private lateinit var binding: MainActivityBinding
|
||||||
private val model: MainViewModel by viewModels()
|
private val model: MainViewModel by viewModels()
|
||||||
|
|
||||||
private var refreshOnResume = false
|
private var refreshOnResume = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = MainActivityBinding.inflate(layoutInflater)
|
binding = MainActivityBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
binding.contents.composeView.setContent {
|
||||||
|
val searchResults: List<ItemInfo> by model.searchResults.observeAsState(emptyList())
|
||||||
|
val source: Source? by model.source.observeAsState(null)
|
||||||
|
val loading: Boolean by model.loading.observeAsState(false)
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
var lastOffset = 0
|
||||||
|
val querySectionHeight = binding.contents.searchview.binding.querySection.root.height.toFloat()
|
||||||
|
|
||||||
|
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { newOffset ->
|
||||||
|
val dy = newOffset - lastOffset
|
||||||
|
lastOffset = newOffset
|
||||||
|
|
||||||
|
binding.contents.searchview.apply {
|
||||||
|
translationY = (translationY - dy).coerceIn(-querySectionHeight .. 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(0.dp, 64.dp, 0.dp, 0.dp)) {
|
||||||
|
item(searchResults) {
|
||||||
|
searchResults.forEach { itemInfo ->
|
||||||
|
ProgressCardView(
|
||||||
|
progress = 0.5f,
|
||||||
|
onClick = {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
this@MainActivity,
|
||||||
|
ReaderActivity::class.java
|
||||||
|
).apply {
|
||||||
|
putExtra("source", model.source.value!!.name)
|
||||||
|
putExtra("id", itemInfo.itemID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
source?.SearchResult(itemInfo = itemInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
if (Preferences["download_folder", ""].isEmpty())
|
if (Preferences["download_folder", ""].isEmpty())
|
||||||
@@ -92,7 +149,7 @@ class MainActivity :
|
|||||||
|
|
||||||
model.availableSortMode.observe(this) {
|
model.availableSortMode.observe(this) {
|
||||||
binding.contents.searchview.post {
|
binding.contents.searchview.post {
|
||||||
binding.contents.searchview.binding.querySection.menuView.menuItems.findMenu(R.id.sort).subMenu.apply {
|
binding.contents.searchview.binding.querySection.menuView.menuItems.findMenu(R.id.sort)?.subMenu?.apply {
|
||||||
clear()
|
clear()
|
||||||
|
|
||||||
it.forEachIndexed { index, sortMode ->
|
it.forEachIndexed { index, sortMode ->
|
||||||
@@ -126,38 +183,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.searchResults.observe(this) {
|
|
||||||
binding.contents.recyclerview.post {
|
|
||||||
if (model.loading) {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
binding.contents.noresult.hide()
|
|
||||||
binding.contents.progressbar.show()
|
|
||||||
|
|
||||||
(binding.contents.recyclerview.adapter as RecyclerSwipeAdapter).run {
|
|
||||||
mItemManger.closeAllItems()
|
|
||||||
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewCompat.animate(binding.contents.searchview)
|
|
||||||
.setDuration(100)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.translationY(0F)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.contents.progressbar.hide()
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
|
|
||||||
binding.contents.noresult.show()
|
|
||||||
} else {
|
|
||||||
binding.contents.recyclerview.adapter?.notifyItemInserted(it.size-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model.suggestions.observe(this) { runOnUiThread {
|
model.suggestions.observe(this) { runOnUiThread {
|
||||||
Logger.d(it)
|
|
||||||
binding.contents.searchview.swapSuggestions(
|
binding.contents.searchview.swapSuggestions(
|
||||||
if (it.isEmpty()) listOf(NoResultSuggestion(getString(R.string.main_no_result))) else it
|
if (it.isEmpty()) listOf(NoResultSuggestion(getString(R.string.main_no_result))) else it
|
||||||
)
|
)
|
||||||
@@ -173,12 +199,6 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
binding.contents.recyclerview.adapter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (binding.drawer.isDrawerOpen(GravityCompat.START))
|
if (binding.drawer.isDrawerOpen(GravityCompat.START))
|
||||||
binding.drawer.closeDrawer(GravityCompat.START)
|
binding.drawer.closeDrawer(GravityCompat.START)
|
||||||
@@ -213,25 +233,6 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
// -height of the search view < translationY < 0
|
|
||||||
binding.contents.searchview.translationY =
|
|
||||||
min(
|
|
||||||
max(
|
|
||||||
binding.contents.searchview.translationY - dy,
|
|
||||||
-binding.contents.searchview.binding.querySection.root.height.toFloat()
|
|
||||||
), 0F)
|
|
||||||
|
|
||||||
if (dy > 0)
|
|
||||||
binding.contents.fab.hideMenuButton(true)
|
|
||||||
else if (dy < 0)
|
|
||||||
binding.contents.fab.showMenuButton(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
|
|
||||||
|
|
||||||
//NavigationView
|
//NavigationView
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
|
|
||||||
@@ -266,15 +267,15 @@ class MainActivity :
|
|||||||
with (binding.contents.randomFab) {
|
with (binding.contents.randomFab) {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
setImageDrawable(CircularProgressDrawable(context))
|
setImageDrawable(CircularProgressDrawable(context))
|
||||||
|
/*
|
||||||
model.random { runOnUiThread {
|
model.random { runOnUiThread {
|
||||||
GalleryDialogFragment(model.source.value!!.name, it.id).apply {
|
GalleryDialogFragment(model.source.value!!.name, it.itemID).apply {
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
model.setQueryAndSearch(it.toQuery())
|
model.setQueryAndSearch(it.toQuery())
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
}.show(supportFragmentManager, "GalleryDialogFragment")
|
||||||
} }
|
} } */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,112 +291,24 @@ class MainActivity :
|
|||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
val galleryID = editText.text.toString()
|
val galleryID = editText.text.toString()
|
||||||
|
/*
|
||||||
GalleryDialogFragment(model.source.value!!.name, galleryID).apply {
|
GalleryDialogFragment(model.source.value!!.name, galleryID).apply {
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
model.setQueryAndSearch(it.toQuery())
|
model.setQueryAndSearch(it.toQuery())
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
}.show(supportFragmentManager, "GalleryDialogFragment")*/
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with (binding.contents.swipePageTurnView) {
|
|
||||||
setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener {
|
|
||||||
override fun onPrev(page: Int) {
|
|
||||||
model.prevPage()
|
|
||||||
|
|
||||||
// disable pageturn until the contents are loaded
|
|
||||||
setCurrentPage(1, false)
|
|
||||||
|
|
||||||
model.query()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNext(page: Int) {
|
|
||||||
model.nextPage()
|
|
||||||
|
|
||||||
// disable pageturn until the contents are loaded
|
|
||||||
setCurrentPage(1, false)
|
|
||||||
|
|
||||||
ViewCompat.animate(binding.contents.searchview)
|
|
||||||
.setDuration(100)
|
|
||||||
.setInterpolator(DecelerateInterpolator())
|
|
||||||
.translationY(0F)
|
|
||||||
|
|
||||||
model.query()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSearchBar()
|
setupSearchBar()
|
||||||
setupRecyclerView()
|
|
||||||
// TODO: Save recent source
|
// TODO: Save recent source
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private fun setupRecyclerView() {
|
|
||||||
with (binding.contents.recyclerview) {
|
|
||||||
adapter = SearchResultsAdapter(model.searchResults).apply {
|
|
||||||
onChipClickedHandler = {
|
|
||||||
model.setQueryAndSearch(it.toQuery())
|
|
||||||
}
|
|
||||||
onDownloadClickedHandler = { source, itemID ->
|
|
||||||
|
|
||||||
closeAllItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteClickedHandler = { source, itemID ->
|
|
||||||
|
|
||||||
closeAllItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ItemClickSupport.addTo(this).apply {
|
|
||||||
onItemClickListener = listener@{ _, position, v ->
|
|
||||||
if (v !is ProgressCardView)
|
|
||||||
return@listener
|
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
|
||||||
putExtra("source", model.source.value!!.name)
|
|
||||||
putExtra("id", model.searchResults.value!![position].id)
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemLongClickListener = listener@{ _, position, v ->
|
|
||||||
if (v !is ProgressCardView)
|
|
||||||
return@listener false
|
|
||||||
|
|
||||||
val result = model.searchResults.value!!.getOrNull(position) ?: return@listener true
|
|
||||||
|
|
||||||
GalleryDialogFragment(model.source.value!!.name, result.id).apply {
|
|
||||||
onChipClickedHandler.add {
|
|
||||||
model.setQueryAndSearch(it.toQuery())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupSearchBar() {
|
private fun setupSearchBar() {
|
||||||
with (binding.contents.searchview) {
|
with (binding.contents.searchview) {
|
||||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
|
||||||
override fun onMenuOpened() {
|
|
||||||
(this@MainActivity.binding.contents.recyclerview.adapter as SearchResultsAdapter).closeAllItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuClosed() {
|
|
||||||
//Do Nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMenuItemClickListener = {
|
onMenuItemClickListener = {
|
||||||
onActionMenuItemSelected(it)
|
onActionMenuItemSelected(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,294 +19,48 @@
|
|||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.core.view.forEach
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.Text
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import com.google.accompanist.appcompattheme.AppCompatTheme
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import com.orhanobut.logger.Logger
|
|
||||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.android.di
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
|
||||||
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
||||||
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
|
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.SavedSourceSet
|
|
||||||
import xyz.quaver.pupil.util.source
|
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity(), DIAware {
|
class ReaderActivity : BaseActivity(), DIAware {
|
||||||
|
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private var source = ""
|
|
||||||
private var itemID = ""
|
|
||||||
|
|
||||||
private var currentPage = 0
|
|
||||||
|
|
||||||
private var isScroll = true
|
|
||||||
private var isFullscreen = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
private lateinit var binding: ReaderActivityBinding
|
private lateinit var binding: ReaderActivityBinding
|
||||||
private val model: ReaderViewModel by viewModels()
|
private val model: ReaderViewModel by viewModels()
|
||||||
|
|
||||||
private val favorites: SavedSourceSet by instance(tag = "favorites")
|
|
||||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.reader_loading)
|
setContent {
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
AppCompatTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar() {
|
||||||
|
Text("Reader")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
handleIntent(intent)
|
}
|
||||||
if (itemID.isEmpty()) {
|
|
||||||
onBackPressed()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.add(source, itemID)
|
|
||||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
|
|
||||||
|
|
||||||
Logger.d(histories)
|
|
||||||
|
|
||||||
model.readerItems.observe(this) {
|
|
||||||
(binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList())
|
|
||||||
|
|
||||||
binding.downloadProgressbar.apply {
|
|
||||||
max = it.size
|
|
||||||
progress = it.count { it.image != null }
|
|
||||||
|
|
||||||
visibility =
|
|
||||||
if (progress == max)
|
|
||||||
View.GONE
|
|
||||||
else
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.title.observe(this) {
|
|
||||||
title = it
|
|
||||||
}
|
|
||||||
|
|
||||||
model.load(source, itemID)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
handleIntent(intent)
|
model.handleIntent(intent)
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent) {
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
|
||||||
val uri = intent.data
|
|
||||||
val lastPathSegment = uri?.lastPathSegment
|
|
||||||
if (uri != null && lastPathSegment != null) {
|
|
||||||
source = uri.host ?: ""
|
|
||||||
itemID = when (uri.host) {
|
|
||||||
"hitomi.la" ->
|
|
||||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: ""
|
|
||||||
"hiyobi.me" -> lastPathSegment
|
|
||||||
"e-hentai.org" -> uri.pathSegments[1]
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
source = intent.getStringExtra("source") ?: ""
|
|
||||||
itemID = intent.getStringExtra("id") ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.reader, menu)
|
|
||||||
|
|
||||||
menu?.forEach {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.reader_menu_favorite -> {
|
|
||||||
if (favorites[source]?.contains(itemID) == true)
|
|
||||||
(it.icon as Animatable).start()
|
|
||||||
}
|
|
||||||
R.id.source -> {
|
|
||||||
it.setIcon(source(source).value.iconResID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.menu = menu
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when(item.itemId) {
|
|
||||||
R.id.reader_menu_favorite -> {
|
|
||||||
val id = itemID
|
|
||||||
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
|
||||||
|
|
||||||
if (favorites[source]?.contains(id) == true) {
|
|
||||||
favorites.remove(source, id)
|
|
||||||
favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
|
|
||||||
} else {
|
|
||||||
favorites.add(source, id)
|
|
||||||
(favorite.icon as Animatable).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (isScroll and !isFullscreen)
|
|
||||||
super.onBackPressed()
|
|
||||||
|
|
||||||
if (isFullscreen) {
|
|
||||||
isFullscreen = false
|
|
||||||
fullscreen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isScroll) {
|
|
||||||
isScroll = true
|
|
||||||
scrollMode(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
return when(keyCode) {
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage+1, 0)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
with (binding.recyclerview) {
|
|
||||||
adapter = ReaderAdapter().apply {
|
|
||||||
onItemClickListener = {
|
|
||||||
if (isScroll) {
|
|
||||||
isScroll = false
|
|
||||||
isFullscreen = true
|
|
||||||
|
|
||||||
scrollMode(false)
|
|
||||||
fullscreen(true)
|
|
||||||
} else {
|
|
||||||
binding.recyclerview.layoutManager?.scrollToPosition(currentPage+1) // Moves to next page because currentPage is 1-based indexing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
|
||||||
|
|
||||||
if (dy < 0)
|
|
||||||
binding.fab.showMenuButton(true)
|
|
||||||
else if (dy > 0)
|
|
||||||
binding.fab.hideMenuButton(true)
|
|
||||||
|
|
||||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
|
||||||
|
|
||||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
|
||||||
return
|
|
||||||
|
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "${currentPage+1}/${recyclerView.adapter!!.itemCount}"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
itemAnimator = null
|
|
||||||
}
|
|
||||||
|
|
||||||
with (binding.retryFab) {
|
|
||||||
setImageResource(R.drawable.refresh)
|
|
||||||
setOnClickListener {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with (binding.fullscreenFab) {
|
|
||||||
setImageResource(R.drawable.ic_fullscreen)
|
|
||||||
setOnClickListener {
|
|
||||||
isFullscreen = true
|
|
||||||
fullscreen(isFullscreen)
|
|
||||||
|
|
||||||
binding.fab.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fullscreen(isFullscreen: Boolean) {
|
|
||||||
(binding.recyclerview.adapter as ReaderAdapter).fullscreen = isFullscreen
|
|
||||||
|
|
||||||
with (window.attributes) {
|
|
||||||
if (isFullscreen) {
|
|
||||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
supportActionBar?.hide()
|
|
||||||
binding.fab.visibility = View.INVISIBLE
|
|
||||||
binding.scroller.let {
|
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
|
|
||||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.HORIZONTAL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
|
||||||
supportActionBar?.show()
|
|
||||||
binding.fab.visibility = View.VISIBLE
|
|
||||||
binding.scroller.let {
|
|
||||||
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
|
|
||||||
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
|
|
||||||
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
|
|
||||||
it.fastScrollDirection = RecyclerViewFastScroller.FastScrollDirection.VERTICAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.attributes = this
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerview.adapter = binding.recyclerview.adapter // Force to redraw
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollMode(isScroll: Boolean) {
|
|
||||||
if (isScroll) {
|
|
||||||
snapHelper.attachToRecyclerView(null)
|
|
||||||
binding.recyclerview.layoutManager = LinearLayoutManager(this)
|
|
||||||
} else {
|
|
||||||
snapHelper.attachToRecyclerView(binding.recyclerview)
|
|
||||||
binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
|
|
||||||
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
|
|
||||||
extraLayoutSpace[0] = 10
|
|
||||||
extraLayoutSpace[1] = 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package xyz.quaver.pupil.ui.composable
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material.FloatingActionButton
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
||||||
|
enum class FloatingActionButtonState(val isExpanded: Boolean) {
|
||||||
|
COLLAPSED(false), EXPANDED(true);
|
||||||
|
|
||||||
|
operator fun not() = lookupTable[!this.isExpanded]!!
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val lookupTable = mapOf(
|
||||||
|
false to COLLAPSED,
|
||||||
|
true to EXPANDED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubFabItem(
|
||||||
|
val icon: ImageVector,
|
||||||
|
val label: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun MultipleFloatingActionButton(
|
||||||
|
items: List<SubFabItem>,
|
||||||
|
fabIcon: ImageVector = Icons.Default.Add,
|
||||||
|
onItemClicked: () -> Unit = { },
|
||||||
|
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
|
||||||
|
onStateChanged: ((FloatingActionButtonState) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val transition = updateTransition(targetState = targetState, label = "expand")
|
||||||
|
|
||||||
|
Column {
|
||||||
|
FloatingActionButton(onClick = {
|
||||||
|
onStateChanged?.invoke(!targetState)
|
||||||
|
}) {
|
||||||
|
items.forEach {
|
||||||
|
|
||||||
|
}
|
||||||
|
Icon(imageVector = fabIcon, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
||||||
import xyz.quaver.pupil.sources.Hitomi
|
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
|
||||||
|
|||||||
@@ -18,50 +18,14 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.drawable.Animatable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout.LayoutParams
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.forEach
|
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco
|
|
||||||
import com.facebook.drawee.controller.BaseControllerListener
|
|
||||||
import com.facebook.imagepipeline.image.ImageInfo
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.x.closestDI
|
import org.kodein.di.android.x.closestDI
|
||||||
import org.kodein.di.instance
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
|
||||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
|
||||||
import xyz.quaver.pupil.databinding.*
|
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import xyz.quaver.pupil.ui.view.TagChip
|
|
||||||
import xyz.quaver.pupil.ui.viewmodel.GalleryDialogViewModel
|
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
|
||||||
import xyz.quaver.pupil.util.SavedSourceSet
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class GalleryDialogFragment(private val source: String, private val itemID: String) : DialogFragment(), DIAware {
|
class GalleryDialogFragment(private val source: String, private val itemID: String) : DialogFragment(), DIAware {
|
||||||
|
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
/*
|
||||||
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
|
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||||
@@ -111,7 +75,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
MainScope().launch {
|
MainScope().launch {
|
||||||
binding.type.text = it.extra[ItemInfo.ExtraType.TYPE]?.await()?.wordCapitalize()
|
binding.type.text = it.extra[ItemInfo.ExtraType.TYPE]?.wordCapitalize()
|
||||||
addDetails(it)
|
addDetails(it)
|
||||||
addPreviews(it)
|
addPreviews(it)
|
||||||
|
|
||||||
@@ -150,11 +114,11 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
|||||||
).zip(
|
).zip(
|
||||||
listOf(
|
listOf(
|
||||||
info.artists.split(", ").map { Tag("artist", it) },
|
info.artists.split(", ").map { Tag("artist", it) },
|
||||||
info.extra[ItemInfo.ExtraType.GROUP]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("group", it) },
|
info.extra[ItemInfo.ExtraType.GROUP]?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("group", it) },
|
||||||
info.extra[ItemInfo.ExtraType.LANGUAGE]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("language", it) },
|
info.extra[ItemInfo.ExtraType.LANGUAGE]?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("language", it) },
|
||||||
info.extra[ItemInfo.ExtraType.SERIES]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("series", it) },
|
info.extra[ItemInfo.ExtraType.SERIES]?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("series", it) },
|
||||||
info.extra[ItemInfo.ExtraType.CHARACTER]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("character", it) },
|
info.extra[ItemInfo.ExtraType.CHARACTER]?.split(", ")?.filterNot { it.isEmpty() }?.map { Tag("character", it) },
|
||||||
info.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.sortedBy {
|
info.extra[ItemInfo.ExtraType.TAGS]?.split(", ")?.filterNot { it.isEmpty() }?.sortedBy {
|
||||||
val tag = Tag.parse(it)
|
val tag = Tag.parse(it)
|
||||||
|
|
||||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
||||||
@@ -197,7 +161,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun addPreviews(info: ItemInfo) {
|
private suspend fun addPreviews(info: ItemInfo) {
|
||||||
val previews = info.extra[ItemInfo.ExtraType.PREVIEW]?.await()?.split(", ") ?: return
|
val previews = info.extra[ItemInfo.ExtraType.PREVIEW]?.split(", ") ?: return
|
||||||
|
|
||||||
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
|
||||||
type.setText(R.string.gallery_thumbnails)
|
type.setText(R.string.gallery_thumbnails)
|
||||||
@@ -257,5 +221,5 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
|||||||
binding.contents.forEach { if (it is RecyclerView) ItemClickSupport.removeFrom(it) }
|
binding.contents.forEach { if (it is RecyclerView) ItemClickSupport.removeFrom(it) }
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ class SourceSelectDialog : DialogFragment(), DIAware {
|
|||||||
var onSourceSelectedListener: ((String) -> Unit)? = null
|
var onSourceSelectedListener: ((String) -> Unit)? = null
|
||||||
var onSourceSettingsSelectedListener: ((String) -> Unit)? = null
|
var onSourceSettingsSelectedListener: ((String) -> Unit)? = null
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {/*
|
||||||
return Dialog(requireContext()).apply {
|
return Dialog(requireContext()).apply {
|
||||||
window?.requestFeature(Window.FEATURE_NO_TITLE)
|
window?.requestFeature(Window.FEATURE_NO_TITLE)
|
||||||
window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||||
@@ -55,6 +55,6 @@ class SourceSelectDialog : DialogFragment(), DIAware {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
*/return super.onCreateDialog(savedInstanceState)}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,9 +45,8 @@ import org.kodein.di.direct
|
|||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.SavedSourceSet
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
/*
|
||||||
class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
|
class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
|
||||||
|
|
||||||
private lateinit var progressDrawable: CircularProgressDrawable
|
private lateinit var progressDrawable: CircularProgressDrawable
|
||||||
@@ -155,4 +154,4 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}*/
|
||||||
@@ -37,9 +37,6 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by instance()
|
private val downloadManager: DownloadManager by instance()
|
||||||
private val cache: ImageCache by instance()
|
|
||||||
|
|
||||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
||||||
@@ -54,9 +51,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
this ?: return false
|
this ?: return false
|
||||||
|
|
||||||
when (key) {
|
when (key) {
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {/*
|
||||||
val cache: ImageCache by instance()
|
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
setMessage(R.string.settings_clear_cache_alert_message)
|
setMessage(R.string.settings_clear_cache_alert_message)
|
||||||
@@ -72,7 +67,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
}.show()
|
}.show()*/
|
||||||
}
|
}
|
||||||
"delete_downloads" -> {
|
"delete_downloads" -> {
|
||||||
val dir = downloadManager.downloadFolder
|
val dir = downloadManager.downloadFolder
|
||||||
@@ -114,6 +109,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
"clear_history" -> {
|
"clear_history" -> {
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -124,7 +120,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}*/
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,12 +131,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
private fun initPreferences() {
|
private fun initPreferences() {
|
||||||
val context = context ?: return
|
val context = context ?: return
|
||||||
|
|
||||||
with (findPreference<Preference>("delete_cache")) {
|
with (findPreference<Preference>("delete_cache")) {/*
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
|
summary = context.getString(R.string.settings_storage_usage, byteToString(cache.cacheFolder.size()))
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
onPreferenceClickListener = this@ManageStorageFragment
|
||||||
}
|
*/}
|
||||||
|
|
||||||
with (findPreference<Preference>("delete_downloads")) {
|
with (findPreference<Preference>("delete_downloads")) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
@@ -163,14 +159,14 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
|||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
onPreferenceClickListener = this@ManageStorageFragment
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
with (findPreference<Preference>("clear_history")) {
|
with (findPreference<Preference>("clear_history")) {
|
||||||
this ?: return@with
|
this ?: return@with
|
||||||
|
|
||||||
summary = context.getString(R.string.settings_clear_history_summary, histories.map.values.sumOf { it.size })
|
summary = context.getString(R.string.settings_clear_history_summary, histories.map.values.sumOf { it.size })
|
||||||
|
|
||||||
onPreferenceClickListener = this@ManageStorageFragment
|
onPreferenceClickListener = this@ManageStorageFragment
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.x.closestDI
|
import org.kodein.di.android.x.closestDI
|
||||||
import org.kodein.di.direct
|
import org.kodein.di.direct
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.pupil.sources.SourcePreferenceIDs
|
|
||||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialogFragment
|
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialogFragment
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.getAvailableLanguages
|
import xyz.quaver.pupil.util.getAvailableLanguages
|
||||||
@@ -49,7 +48,7 @@ class SourceSettingsFragment(private val source: String) :
|
|||||||
private val client: HttpClient by instance()
|
private val client: HttpClient by instance()
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(direct.instance<SourcePreferenceIDs>().toMap()[source]!!, rootKey)
|
/*setPreferencesFromResource(direct.instance<SourcePreferenceIDs>().toMap()[source]!!, rootKey)*/
|
||||||
|
|
||||||
initPreferences()
|
initPreferences()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,11 @@ import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
|||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.sources.Hitomi
|
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||||
import xyz.quaver.pupil.types.FavoriteHistorySwitch
|
import xyz.quaver.pupil.types.FavoriteHistorySwitch
|
||||||
import xyz.quaver.pupil.types.HistorySuggestion
|
import xyz.quaver.pupil.types.HistorySuggestion
|
||||||
import xyz.quaver.pupil.types.LoadingSuggestion
|
import xyz.quaver.pupil.types.LoadingSuggestion
|
||||||
import xyz.quaver.pupil.types.NoResultSuggestion
|
import xyz.quaver.pupil.types.NoResultSuggestion
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
xyz.quaver.floatingsearchview.FloatingSearchView(context, attrs),
|
xyz.quaver.floatingsearchview.FloatingSearchView(context, attrs),
|
||||||
|
|||||||
@@ -6,10 +6,41 @@ 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.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
|
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import xyz.quaver.pupil.sources.Source
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProgressCardView(progress: Float? = null, onLongClick: (() -> Unit)? = null, onClick: () -> Unit, content: @Composable () -> Unit) {
|
||||||
|
MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
elevation = 4.dp
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth()) }
|
||||||
|
content.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ProgressCardView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
|
class ProgressCardView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.view
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.android.closestDI
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.sources.Hitomi
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.util.SavedSourceSet
|
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
|
||||||
class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(context), DIAware {
|
|
||||||
|
|
||||||
override val di by closestDI(context)
|
|
||||||
|
|
||||||
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
|
|
||||||
// TODO private val translations: Map<String, String> by instance()
|
|
||||||
|
|
||||||
val tag: Tag =
|
|
||||||
_tag.let {
|
|
||||||
when {
|
|
||||||
it.area != null -> it
|
|
||||||
else -> Tag("tag", _tag.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
when(tag.area) {
|
|
||||||
"male" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_blue_700)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
setCloseIconTintResource(android.R.color.white)
|
|
||||||
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white)
|
|
||||||
}
|
|
||||||
"female" -> {
|
|
||||||
setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
|
||||||
setCloseIconTintResource(android.R.color.white)
|
|
||||||
chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
|
||||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
|
||||||
|
|
||||||
isCloseIconVisible = true
|
|
||||||
closeIcon = ContextCompat.getDrawable(context,
|
|
||||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
|
||||||
R.drawable.ic_star_filled
|
|
||||||
else
|
|
||||||
R.drawable.ic_star_empty
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnCloseIconClickListener {
|
|
||||||
if (favoriteTags[source]?.contains(tag.toString()) == true) {
|
|
||||||
favoriteTags.remove(source, tag.toString())
|
|
||||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
|
|
||||||
|
|
||||||
when(tag.area) {
|
|
||||||
"male" -> setChipBackgroundColorResource(R.color.material_blue_700)
|
|
||||||
"female" -> setChipBackgroundColorResource(R.color.material_pink_600)
|
|
||||||
else -> chipBackgroundColor = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
favoriteTags.add(source, tag.toString())
|
|
||||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
|
|
||||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text = when (tag.area) {
|
|
||||||
// TODO languageMap
|
|
||||||
"language" -> Hitomi.languageMap[tag.tag]
|
|
||||||
else -> /*(translations[tag.tag] ?: */tag.tag.wordCapitalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.TypedArray
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import com.google.android.material.chip.ChipGroup
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.types.Tag
|
|
||||||
import xyz.quaver.pupil.types.Tags
|
|
||||||
|
|
||||||
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, var source: String = "hitomi.la", val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
|
|
||||||
|
|
||||||
object Defaults {
|
|
||||||
const val maxChipSize = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxChipSize: Int = Defaults.maxChipSize
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val moreView = Chip(context).apply {
|
|
||||||
text = "…"
|
|
||||||
|
|
||||||
setEnsureMinTouchTargetSize(false)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
removeView(this)
|
|
||||||
|
|
||||||
for (i in maxChipSize until tags.size) {
|
|
||||||
val tag = tags.elementAt(i)
|
|
||||||
|
|
||||||
addView(TagChip(context, source, tag).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onClickListener?.invoke(tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onClickListener: ((Tag) -> Unit)? = null
|
|
||||||
|
|
||||||
private fun applyAttributes(attr: TypedArray) {
|
|
||||||
maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var refreshJob: Job? = null
|
|
||||||
fun refresh() {
|
|
||||||
refreshJob?.cancel()
|
|
||||||
this.removeAllViews()
|
|
||||||
|
|
||||||
refreshJob = CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
tags.take(maxChipSize).map {
|
|
||||||
CoroutineScope(Dispatchers.Default).async {
|
|
||||||
TagChip(context, source, it).apply {
|
|
||||||
setOnClickListener {
|
|
||||||
onClickListener?.invoke(this.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
addView(it.await())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxChipSize > 0 && tags.size > maxChipSize)
|
|
||||||
addView(moreView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
applyAttributes(context.obtainStyledAttributes(attr, R.styleable.TagChipGroup))
|
|
||||||
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -40,12 +40,12 @@ class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware
|
|||||||
private val _related = MutableLiveData<List<ItemInfo>>()
|
private val _related = MutableLiveData<List<ItemInfo>>()
|
||||||
val related: LiveData<List<ItemInfo>> = _related
|
val related: LiveData<List<ItemInfo>> = _related
|
||||||
|
|
||||||
fun load(source: String, itemID: String) {
|
fun load(source: String, itemID: String) {/*
|
||||||
val source: Source by source(source)
|
val source: Source by source(source)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_info.value = withContext(Dispatchers.IO) {
|
_info.value = withContext(Dispatchers.IO) {
|
||||||
source.info(itemID).also { it.awaitAll() }
|
source.info(itemID)
|
||||||
}.also {
|
}.also {
|
||||||
_related.value = it.extra[ItemInfo.ExtraType.RELATED_ITEM]?.await()?.split(", ")?.map { related ->
|
_related.value = it.extra[ItemInfo.ExtraType.RELATED_ITEM]?.await()?.split(", ")?.map { related ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
@@ -53,7 +53,7 @@ class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware
|
|||||||
}
|
}
|
||||||
}?.awaitAll()
|
}?.awaitAll()
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,6 @@ import org.kodein.di.instance
|
|||||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import xyz.quaver.pupil.sources.*
|
import xyz.quaver.pupil.sources.*
|
||||||
import xyz.quaver.pupil.util.Preferences
|
import xyz.quaver.pupil.util.Preferences
|
||||||
import xyz.quaver.pupil.util.notify
|
|
||||||
import xyz.quaver.pupil.util.source
|
import xyz.quaver.pupil.util.source
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -39,10 +38,11 @@ import kotlin.random.Random
|
|||||||
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private val _searchResults = MutableLiveData<MutableList<ItemInfo>>()
|
private val _searchResults = MutableLiveData<List<ItemInfo>>()
|
||||||
val searchResults = _searchResults as LiveData<List<ItemInfo>>
|
val searchResults = _searchResults as LiveData<List<ItemInfo>>
|
||||||
var loading = false
|
|
||||||
private set
|
private val _loading = MutableLiveData(false)
|
||||||
|
val loading = _loading as LiveData<Boolean>
|
||||||
|
|
||||||
private var queryJob: Job? = null
|
private var queryJob: Job? = null
|
||||||
private var suggestionJob: Job? = null
|
private var suggestionJob: Job? = null
|
||||||
@@ -111,7 +111,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
setSourceAndReset(
|
setSourceAndReset(
|
||||||
when {
|
when {
|
||||||
mode == MainMode.DOWNLOADS -> "downloads"
|
mode == MainMode.DOWNLOADS -> "downloads"
|
||||||
source.value is Downloads -> "hitomi.la"
|
//source.value is Downloads -> "hitomi.la"
|
||||||
else -> source.value!!.name
|
else -> source.value!!.name
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
suggestionJob?.cancel()
|
suggestionJob?.cancel()
|
||||||
queryJob?.cancel()
|
queryJob?.cancel()
|
||||||
|
|
||||||
loading = true
|
_loading.value = true
|
||||||
val results = mutableListOf<ItemInfo>()
|
val results = mutableListOf<ItemInfo>()
|
||||||
_searchResults.value = results
|
_searchResults.value = results
|
||||||
|
|
||||||
@@ -146,11 +146,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
for (result in channel) {
|
for (result in channel) {
|
||||||
yield()
|
yield()
|
||||||
results.add(result)
|
results.add(result)
|
||||||
_searchResults.notify()
|
_searchResults.value = results.toList()
|
||||||
}
|
}
|
||||||
_searchResults.notify()
|
|
||||||
|
|
||||||
loading = false
|
_loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,20 @@
|
|||||||
package xyz.quaver.pupil.ui.viewmodel
|
package xyz.quaver.pupil.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.orhanobut.logger.Logger
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.util.*
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.x.closestDI
|
import org.kodein.di.android.x.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import xyz.quaver.pupil.adapters.ReaderItem
|
import xyz.quaver.pupil.adapters.ReaderItem
|
||||||
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
|
import xyz.quaver.pupil.db.Bookmark
|
||||||
|
import xyz.quaver.pupil.db.History
|
||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
import xyz.quaver.pupil.util.ImageCache
|
|
||||||
import xyz.quaver.pupil.util.notify
|
import xyz.quaver.pupil.util.notify
|
||||||
import xyz.quaver.pupil.util.source
|
import xyz.quaver.pupil.util.source
|
||||||
|
|
||||||
@@ -44,7 +42,16 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
|
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private val cache: ImageCache by instance()
|
private val database: AppDatabase by instance()
|
||||||
|
|
||||||
|
private val historyDao = database.historyDao()
|
||||||
|
private val bookmarkDao = database.bookmarkDao()
|
||||||
|
|
||||||
|
private val _source = MutableLiveData<String>()
|
||||||
|
val source = _source as LiveData<String>
|
||||||
|
|
||||||
|
private val _itemID = MutableLiveData<String>()
|
||||||
|
val itemID = _itemID as LiveData<String>
|
||||||
|
|
||||||
private val _title = MutableLiveData<String>()
|
private val _title = MutableLiveData<String>()
|
||||||
val title = _title as LiveData<String>
|
val title = _title as LiveData<String>
|
||||||
@@ -55,9 +62,56 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
private var _readerItems = MutableLiveData<MutableList<ReaderItem>>()
|
private var _readerItems = MutableLiveData<MutableList<ReaderItem>>()
|
||||||
val readerItems = _readerItems as LiveData<List<ReaderItem>>
|
val readerItems = _readerItems as LiveData<List<ReaderItem>>
|
||||||
|
|
||||||
|
val isBookmarked = Transformations.switchMap(MediatorLiveData<Pair<Source, String>>().apply {
|
||||||
|
addSource(source) { source -> itemID.value?.let { itemID -> source to itemID } }
|
||||||
|
addSource(itemID) { itemID -> source.value?.let { source -> source to itemID } }
|
||||||
|
}) { (source, itemID) ->
|
||||||
|
bookmarkDao.contains(source.name, itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceInstance = Transformations.map(source) {
|
||||||
|
source(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceIcon = Transformations.map(sourceInstance) {
|
||||||
|
it.value.iconResID
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses source and itemID from the intent
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException when the intent has no recognizable source and/or itemID
|
||||||
|
*/
|
||||||
|
fun handleIntent(intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_VIEW) {
|
||||||
|
val uri = intent.data
|
||||||
|
val lastPathSegment = uri?.lastPathSegment
|
||||||
|
if (uri != null && lastPathSegment != null) {
|
||||||
|
_source.postValue(uri.host ?: error("Source cannot be null"))
|
||||||
|
_itemID.postValue(when (uri.host) {
|
||||||
|
"hitomi.la" ->
|
||||||
|
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: error("Invalid itemID")
|
||||||
|
"hiyobi.me" -> lastPathSegment
|
||||||
|
"e-hentai.org" -> uri.pathSegments[1]
|
||||||
|
else -> error("Invalid host")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_source.postValue(intent.getStringExtra("source") ?: error("Invalid source"))
|
||||||
|
_itemID.postValue(intent.getStringExtra("id") ?: error("Invalid itemID"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun load(sourceName: String, itemID: String) {
|
fun load() {
|
||||||
val source: Source by source(sourceName)
|
val source: Source by source(source.value ?: return)
|
||||||
|
val itemID = itemID.value ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
historyDao.insert(History(source.name, itemID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_title.value = withContext(Dispatchers.IO) {
|
_title.value = withContext(Dispatchers.IO) {
|
||||||
@@ -66,66 +120,20 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
_images.postValue(withContext(Dispatchers.IO) {
|
||||||
source.images(itemID)
|
source.images(itemID)
|
||||||
}.let { images ->
|
})
|
||||||
_readerItems.value = MutableList(images.size) { ReaderItem(0F, null) }
|
|
||||||
_images.value = images
|
|
||||||
|
|
||||||
images.forEachIndexed { index, image ->
|
|
||||||
when (val scheme = image.takeWhile { it != ':' }) {
|
|
||||||
"http", "https" -> {
|
|
||||||
val file = cache.load {
|
|
||||||
url(image)
|
|
||||||
headers(source.getHeadersBuilderForImage(itemID, image))
|
|
||||||
}
|
|
||||||
|
|
||||||
val channel = cache.channels[image] ?: error("Channel is null")
|
|
||||||
|
|
||||||
if (channel.isClosedForReceive) {
|
|
||||||
_readerItems.value!![index] =
|
|
||||||
ReaderItem(_readerItems.value!![index].progress, Uri.fromFile(file))
|
|
||||||
_readerItems.notify()
|
|
||||||
} else {
|
|
||||||
channel.invokeOnClose { e ->
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (e == null) {
|
|
||||||
_readerItems.value!![index] =
|
|
||||||
ReaderItem(_readerItems.value!![index].progress, Uri.fromFile(file))
|
|
||||||
_readerItems.notify()
|
|
||||||
} else {
|
|
||||||
Logger.e(index.toString())
|
|
||||||
Logger.e(e, e.message ?: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
|
||||||
kotlin.runCatching {
|
|
||||||
for (progress in channel) {
|
|
||||||
_readerItems.value!![index] =
|
|
||||||
ReaderItem(progress, _readerItems.value!![index].image)
|
|
||||||
_readerItems.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"content" -> {
|
|
||||||
_readerItems.value!![index] = ReaderItem(100f, Uri.parse(image))
|
|
||||||
_readerItems.notify()
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
fun toggleBookmark() {
|
||||||
|
val bookmark = source.value?.let { source -> itemID.value?.let { itemID -> Bookmark(source, itemID) } } ?: return
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
cache.cleanup()
|
if (bookmarkDao.contains(bookmark).value ?: return@launch)
|
||||||
images.value?.let { cache.free(it) }
|
bookmarkDao.delete(bookmark)
|
||||||
|
else
|
||||||
|
bookmarkDao.insert(bookmark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.FileX
|
||||||
import xyz.quaver.io.util.*
|
import xyz.quaver.io.util.*
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
import xyz.quaver.pupil.sources.Source
|
import xyz.quaver.pupil.sources.Source
|
||||||
|
|
||||||
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
|
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 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.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.features.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.utils.io.*
|
|
||||||
import io.ktor.utils.io.core.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.android.closestDI
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import xyz.quaver.io.FileX
|
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
class ImageCache(context: Context) : DIAware {
|
|
||||||
override val di by closestDI(context)
|
|
||||||
|
|
||||||
private val applicationContext: Pupil by instance()
|
|
||||||
private val client: HttpClient by instance()
|
|
||||||
|
|
||||||
val cacheFolder = File(context.cacheDir, "imageCache")
|
|
||||||
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
|
|
||||||
|
|
||||||
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
|
|
||||||
val channels = _channels as Map<String, Channel<Float>>
|
|
||||||
|
|
||||||
private val requests = mutableMapOf<String, Job>()
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
suspend fun cleanup() = coroutineScope {
|
|
||||||
val LIMIT = 100*1024*1024
|
|
||||||
|
|
||||||
cacheFolder.listFiles { it -> it.canonicalPath !in cache.values || it.name == ".cache" }?.forEach { it.delete() }
|
|
||||||
|
|
||||||
if (cacheFolder.size() > LIMIT)
|
|
||||||
do {
|
|
||||||
cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
|
|
||||||
File(it.value).delete()
|
|
||||||
cache.remove(it.key)
|
|
||||||
}
|
|
||||||
} while (cacheFolder.size() > LIMIT / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun free(images: List<String>) {
|
|
||||||
images.forEach {
|
|
||||||
requests[it]?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
images.forEach { _channels.remove(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
suspend fun clear() = coroutineScope {
|
|
||||||
requests.values.forEach { it.cancel() }
|
|
||||||
cacheFolder.listFiles()?.forEach { it.delete() }
|
|
||||||
cache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File {
|
|
||||||
val request = HttpRequestBuilder().apply(requestBuilder)
|
|
||||||
|
|
||||||
val key = request.url.buildString()
|
|
||||||
|
|
||||||
val progressChannel = if (_channels[key]?.isClosedForSend == false)
|
|
||||||
_channels[key]!!
|
|
||||||
else
|
|
||||||
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
|
|
||||||
|
|
||||||
return cache[key]?.let {
|
|
||||||
progressChannel.close()
|
|
||||||
File(it)
|
|
||||||
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
|
|
||||||
if (!file.exists())
|
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
cache[key] = file.canonicalPath
|
|
||||||
|
|
||||||
requests[key] = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
kotlin.runCatching {
|
|
||||||
client.get<HttpStatement>(request).execute { httpResponse ->
|
|
||||||
val responseChannel: ByteReadChannel = httpResponse.receive()
|
|
||||||
val contentLength = httpResponse.contentLength() ?: -1
|
|
||||||
var readBytes = 0F
|
|
||||||
|
|
||||||
while (!responseChannel.isClosedForRead) {
|
|
||||||
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
|
||||||
while (!packet.isEmpty) {
|
|
||||||
val bytes = packet.readBytes()
|
|
||||||
file.appendBytes(bytes)
|
|
||||||
readBytes += bytes.size
|
|
||||||
progressChannel.trySend(readBytes / contentLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progressChannel.close()
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
file.delete()
|
|
||||||
cache.remove(key)
|
|
||||||
FirebaseCrashlytics.getInstance().recordException(it)
|
|
||||||
progressChannel.close(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.util
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.builtins.MapSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.Json.Default.decodeFromString
|
|
||||||
import kotlinx.serialization.serializer
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V, private val map: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map {
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
val serializer: KSerializer<Map<K, V>> = MapSerializer(serializer(anyKey::class.java) as KSerializer<K>, serializer(anyValue::class.java) as KSerializer<V>)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (!file.exists()) {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun load() {
|
|
||||||
map.clear()
|
|
||||||
kotlin.runCatching {
|
|
||||||
decodeFromString(serializer, file.readText())
|
|
||||||
}.onSuccess {
|
|
||||||
map.putAll(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun save() {
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
if (!file.exists())
|
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
file.writeText(Json.encodeToString(serializer, map))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun put(key: K, value: V): V? {
|
|
||||||
map.remove(key)
|
|
||||||
|
|
||||||
return map.put(key, value).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun putAll(from: Map<out K, V>) {
|
|
||||||
for (key in from.keys) {
|
|
||||||
map.remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
map.putAll(from)
|
|
||||||
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun remove(key: K): V? {
|
|
||||||
return map.remove(key).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
@RequiresApi(24)
|
|
||||||
override fun remove(key: K, value: V): Boolean {
|
|
||||||
return map.remove(key, value).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clear() {
|
|
||||||
map.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class SavedSourceSet(private val file: File) {
|
|
||||||
private val _map = mutableMapOf<String, MutableList<String>>()
|
|
||||||
val map: Map<String, List<String>> = _map
|
|
||||||
|
|
||||||
private val serializer = MapSerializer(String.serializer(), ListSerializer(String.serializer()))
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun load() {
|
|
||||||
_map.clear()
|
|
||||||
kotlin.runCatching {
|
|
||||||
decodeFromString(serializer, file.readText())
|
|
||||||
}.onSuccess {
|
|
||||||
it.forEach { (k, v) ->
|
|
||||||
_map[k] = v.toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun save() {
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
if (!file.exists())
|
|
||||||
file.createNewFile()
|
|
||||||
|
|
||||||
file.writeText(Json.encodeToString(serializer, _map))
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun get(key: String) = _map[key]
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun add(source: String, value: String) {
|
|
||||||
_map[source]?.remove(value)
|
|
||||||
|
|
||||||
if (!_map.containsKey(source))
|
|
||||||
_map[source] = mutableListOf(value)
|
|
||||||
else
|
|
||||||
_map[source]!!.add(value)
|
|
||||||
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun addAll(from: Map<String, Set<String>>) {
|
|
||||||
for (source in from.keys) {
|
|
||||||
if (_map.containsKey(source)) {
|
|
||||||
_map[source]!!.removeAll(from[source]!!)
|
|
||||||
_map[source]!!.addAll(from[source]!!)
|
|
||||||
} else {
|
|
||||||
_map[source] = from[source]!!.toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun remove(source: String, value: String): Boolean {
|
|
||||||
return (_map[source]?.remove(value) ?: false).also {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun clear() {
|
|
||||||
_map.clear()
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -43,7 +43,7 @@ fun hashWithSalt(password: String): Pair<String, String> {
|
|||||||
return Pair(hash(password+salt), salt)
|
return Pair(hash(password+salt), salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
private const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.DirectDIAware
|
import org.kodein.di.DirectDIAware
|
||||||
import org.kodein.di.direct
|
import org.kodein.di.direct
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
import xyz.quaver.pupil.sources.SourceEntries
|
import xyz.quaver.pupil.sources.SourceEntries
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -40,7 +41,7 @@ fun String.wordCapitalize() : String {
|
|||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@SuppressLint("DefaultLocale")
|
||||||
for (word in this.split(" "))
|
for (word in this.split(" "))
|
||||||
result.add(word.capitalize(Locale.US))
|
result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() })
|
||||||
|
|
||||||
return result.joinToString(" ")
|
return result.joinToString(" ")
|
||||||
}
|
}
|
||||||
@@ -73,9 +74,8 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
|
|||||||
fun Int.normalizeID() = this.and(0xFFFF)
|
fun Int.normalizeID() = this.and(0xFFFF)
|
||||||
|
|
||||||
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
|
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
|
||||||
"-id-" to { id },
|
"-id-" to { itemID },
|
||||||
"-title-" to { title },
|
"-title-" to { title },
|
||||||
"-artist-" to { artists }
|
|
||||||
// TODO
|
// TODO
|
||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
@@ -103,8 +103,8 @@ operator fun JsonElement.get(tag: String) =
|
|||||||
val JsonElement.content
|
val JsonElement.content
|
||||||
get() = this.jsonPrimitive.contentOrNull
|
get() = this.jsonPrimitive.contentOrNull
|
||||||
|
|
||||||
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
|
fun List<MenuItem>.findMenu(itemID: Int): MenuItem? {
|
||||||
return first { it.itemId == itemID }
|
return firstOrNull { it.itemId == itemID }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <E> MutableLiveData<MutableList<E>>.notify() {
|
fun <E> MutableLiveData<MutableList<E>>.notify() {
|
||||||
@@ -127,6 +127,9 @@ fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytes
|
|||||||
fun DIAware.source(source: String) = lazy { direct.source(source) }
|
fun DIAware.source(source: String) = lazy { direct.source(source) }
|
||||||
fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!!
|
fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!!
|
||||||
|
|
||||||
|
fun DIAware.database() = lazy { direct.database() }
|
||||||
|
fun DirectDIAware.database() = instance<AppDatabase>()
|
||||||
|
|
||||||
fun View.hide() {
|
fun View.hide() {
|
||||||
visibility = View.INVISIBLE
|
visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,52 +24,10 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.MainActivity">
|
tools:context=".ui.MainActivity">
|
||||||
|
|
||||||
<xyz.quaver.pupil.ui.view.SwipePageTurnView
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/swipe_page_turn_view"
|
android:id="@+id/compose_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
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:handleDrawable="@drawable/thumb"
|
|
||||||
app:handleHasFixedSize="true"
|
|
||||||
app:handleHeight="72dp"
|
|
||||||
app:handleWidth="24dp"
|
|
||||||
app:disableTrack="true"
|
|
||||||
app:hideHandleAfter="1000"
|
|
||||||
app:trackMarginStart="64dp"
|
|
||||||
app:addLastItemPadding="true"
|
|
||||||
app:popupDrawable="@android:color/transparent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingTop="64dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
|
||||||
|
|
||||||
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
|
|
||||||
|
|
||||||
</xyz.quaver.pupil.ui.view.SwipePageTurnView>
|
|
||||||
|
|
||||||
<androidx.core.widget.ContentLoadingProgressBar
|
|
||||||
style="?android:attr/progressBarStyle"
|
|
||||||
android:id="@+id/progressbar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:indeterminate="true"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/noresult"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/main_no_result"
|
|
||||||
android:linksClickable="true"
|
|
||||||
android:visibility="invisible"/>
|
|
||||||
|
|
||||||
<com.github.clans.fab.FloatingActionMenu
|
<com.github.clans.fab.FloatingActionMenu
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
|
|||||||
@@ -30,112 +30,9 @@
|
|||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
tools:ignore="RtlHardcoded">
|
tools:ignore="RtlHardcoded">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
|
android:id="@+id/compose_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<com.facebook.drawee.view.SimpleDraweeView
|
|
||||||
android:id="@+id/thumbnail"
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/galleryblock_thumbnail_description"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.AppCompat.Headline"
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
style="@style/TextAppearance.AppCompat.Medium"
|
|
||||||
android:id="@+id/artist"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/extra"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/artist"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"/>
|
|
||||||
|
|
||||||
<xyz.quaver.pupil.ui.view.TagChipGroup
|
|
||||||
android:id="@+id/tag_group"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:chipSpacing="4dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/extra"
|
|
||||||
app:layout_constraintLeft_toRightOf="@id/thumbnail"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"/>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/barrier"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="bottom"
|
|
||||||
app:constraint_referenced_ids="thumbnail, tag_group"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?android:attr/listDivider"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
|
||||||
android:layout_margin="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/id_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:maxWidth="150dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/pagecount"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/favorite"
|
|
||||||
android:contentDescription="@string/app_name"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:srcCompat="@drawable/ic_star_empty"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</xyz.quaver.pupil.ui.view.ProgressCardView>
|
</xyz.quaver.pupil.ui.view.ProgressCardView>
|
||||||
@@ -26,6 +26,8 @@ package xyz.quaver.pupil
|
|||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -38,9 +40,9 @@ class ExampleUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
val a = mutableSetOf<Int>()
|
runBlocking {
|
||||||
|
|
||||||
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.0.0-rc01")
|
classpath("com.android.tools.build:gradle:7.0.2")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.21")
|
classpath("org.jetbrains.kotlin:kotlin-android-extensions:1.5.21")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.21")
|
classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.21")
|
||||||
classpath("com.google.gms:google-services:4.3.8")
|
classpath("com.google.gms:google-services:4.3.10")
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
|
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
|
||||||
@@ -23,7 +23,6 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
|
||||||
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
|
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
|
||||||
maven { url = uri("https://jitpack.io") }
|
maven { url = uri("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,4 @@ org.gradle.configureondemand=true
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableBuildCache=true
|
|
||||||
Reference in New Issue
Block a user