From 067a263336d2a6004c22389ad1ab1a5a35a5e790 Mon Sep 17 00:00:00 2001
From: tom5079 <7948651+tom5079@users.noreply.github.com>
Date: Sat, 6 Apr 2024 18:08:22 -0700
Subject: [PATCH] wip
---
app/build.gradle | 11 +-
app/src/main/AndroidManifest.xml | 115 +---
app/src/main/java/xyz/quaver/pupil/Pupil.kt | 50 +-
.../quaver/pupil/adapters/ReaderAdapter.kt | 250 -------
.../xyz/quaver/pupil/di/SingletonModule.kt | 24 +
.../java/xyz/quaver/pupil/hitomi/common.kt | 273 --------
.../java/xyz/quaver/pupil/hitomi/galleries.kt | 54 --
.../xyz/quaver/pupil/hitomi/galleryblock.kt | 90 ---
.../java/xyz/quaver/pupil/hitomi/reader.kt | 38 --
.../java/xyz/quaver/pupil/hitomi/results.kt | 87 ---
.../java/xyz/quaver/pupil/hitomi/search.kt | 328 ----------
.../quaver/pupil/networking/GalleryInfo.kt | 30 +-
.../pupil/networking/HitomiHttpClient.kt | 82 ++-
.../xyz/quaver/pupil/networking/ImageCache.kt | 128 ++++
.../quaver/pupil/services/DownloadService.kt | 445 -------------
.../pupil/services/ImageCacheService.kt | 57 ++
.../java/xyz/quaver/pupil/ui/MainActivity.kt | 3 +-
.../xyz/quaver/pupil/ui/ReaderActivity.kt | 619 ------------------
.../xyz/quaver/pupil/ui/composable/Gallery.kt | 187 ++++--
.../xyz/quaver/pupil/ui/composable/MainApp.kt | 45 +-
.../ui/composable/MainNavigationActions.kt | 7 +-
.../pupil/ui/composable/SearchScreen.kt | 146 ++++-
.../xyz/quaver/pupil/ui/reader/ImageViewer.kt | 2 +
.../pupil/ui/viewmodel/MainViewModel.kt | 12 +-
.../xyz/quaver/pupil/util/downloader/Cache.kt | 297 ---------
.../pupil/util/downloader/DownloadManager.kt | 123 ----
app/src/main/res/layout/reader_activity.xml | 105 ---
app/src/main/res/layout/reader_eye_card.xml | 67 --
app/src/main/res/layout/reader_item.xml | 72 --
app/src/main/res/values-ja/strings.xml | 7 +-
app/src/main/res/values-ko/strings.xml | 7 +-
app/src/main/res/values/strings.xml | 8 +-
build.gradle | 1 +
33 files changed, 610 insertions(+), 3160 deletions(-)
delete mode 100644 app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/di/SingletonModule.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/networking/ImageCache.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/services/ImageCacheService.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/reader/ImageViewer.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
delete mode 100644 app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
delete mode 100644 app/src/main/res/layout/reader_activity.xml
delete mode 100644 app/src/main/res/layout/reader_eye_card.xml
delete mode 100644 app/src/main/res/layout/reader_item.xml
diff --git a/app/build.gradle b/app/build.gradle
index 797e4c52..07ff5e6b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,7 +4,7 @@ apply plugin: "kotlin-parcelize"
apply plugin: "kotlinx-serialization"
apply plugin: "com.google.android.gms.oss-licenses-plugin"
apply plugin: "com.google.devtools.ksp"
-
+apply plugin: "com.google.dagger.hilt.android"
if (file("google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
@@ -82,7 +82,7 @@ dependencies {
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.9.0"
- implementation platform("androidx.compose:compose-bom:2024.02.02")
+ implementation platform("androidx.compose:compose-bom:2024.03.00")
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.material3:material3-window-size-class"
@@ -90,7 +90,7 @@ dependencies {
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.6.2'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.6.4'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.activity:activity-compose:1.8.2'
@@ -112,9 +112,12 @@ dependencies {
implementation "io.coil-kt:coil-compose:2.6.0"
+ implementation "com.google.dagger:hilt-android:2.44"
+ ksp "com.google.dagger:hilt-compiler:2.44"
+
implementation "com.google.android.material:material:1.11.0"
- implementation platform('com.google.firebase:firebase-bom:32.7.4')
+ implementation platform('com.google.firebase:firebase-bom:32.8.0')
implementation "com.google.firebase:firebase-analytics-ktx"
implementation "com.google.firebase:firebase-crashlytics-ktx"
implementation "com.google.firebase:firebase-perf-ktx"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47e0af21..d54999b4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,8 +11,8 @@
-
+
@@ -28,7 +28,8 @@
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
tools:replace="android:theme"
- tools:ignore="UnusedAttribute">
+ tools:ignore="UnusedAttribute"
+ android:dataExtractionRules="@xml/data_extraction_rules">
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -179,7 +77,6 @@
-
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
index 4b645d6f..8a764239 100644
--- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt
+++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
@@ -40,6 +40,7 @@ import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.*
import okhttp3.Dispatcher
import okhttp3.Interceptor
@@ -58,15 +59,6 @@ import kotlin.reflect.KClass
typealias PupilInterceptor = (Interceptor.Chain) -> Response
-lateinit var histories: SavedSet
- private set
-lateinit var favorites: SavedSet
- private set
-lateinit var favoriteTags: SavedSet
- private set
-lateinit var searchHistory: SavedSet
- private set
-
val interceptors = mutableMapOf, PupilInterceptor>()
lateinit var clientBuilder: OkHttpClient.Builder
@@ -77,22 +69,13 @@ val client: OkHttpClient
clientHolder = it
}
-class Pupil : Application(), ImageLoaderFactory {
- companion object {
- lateinit var instance: Pupil
- private set
- }
-
+@HiltAndroidApp
+class Pupil : Application() {
override fun onCreate() {
- instance = this
-
- AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
-
preferences = PreferenceManager.getDefaultSharedPreferences(this)
val userID = Preferences["user_id", ""].let { userID ->
- if (userID.isEmpty()) UUID.randomUUID().toString().also { Preferences["user_id"] = it }
- else userID
+ userID.ifEmpty { UUID.randomUUID().toString().also { Preferences["user_id"] = it } }
}
FirebaseApp.initializeApp(this)
@@ -101,7 +84,6 @@ class Pupil : Application(), ImageLoaderFactory {
val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder()
-// .connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.proxyInfo(proxyInfo)
.addInterceptor { chain ->
@@ -140,19 +122,6 @@ class Pupil : Application(), ImageLoaderFactory {
Preferences["reset_secure"] = true
}
- histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), 0)
- favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), 0)
- favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
- searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
-
- favoriteTags.filter { it.tag.contains('_') }.forEach {
- favoriteTags.remove(it)
- }
-
- /*
- if (BuildConfig.DEBUG)
- FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)*/
-
try {
ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) {
@@ -209,15 +178,4 @@ class Pupil : Application(), ImageLoaderFactory {
super.onCreate()
}
-
- override fun newImageLoader() = ImageLoader
- .Builder(this)
- .okHttpClient {
- OkHttpClient
- .Builder()
- .sslSocketFactory(SSLSettings.sslContext!!.socketFactory, SSLSettings.trustManager!!)
- .build()
- }.memoryCache(null)
- .build()
-
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
deleted file mode 100644
index 93892729..00000000
--- a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
+++ /dev/null
@@ -1,250 +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 .
- */
-
-package xyz.quaver.pupil.adapters
-
-import android.content.Context
-import android.graphics.drawable.Animatable
-import android.net.Uri
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.ContextCompat
-import androidx.core.view.updateLayoutParams
-import androidx.recyclerview.widget.RecyclerView
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.controller.BaseControllerListener
-import com.facebook.drawee.drawable.ScalingUtils
-import com.facebook.drawee.interfaces.DraweeController
-import com.facebook.drawee.view.SimpleDraweeView
-import com.facebook.imagepipeline.image.ImageInfo
-import com.github.piasy.biv.view.BigImageView
-import com.github.piasy.biv.view.ImageShownCallback
-import com.github.piasy.biv.view.ImageViewFactory
-import kotlinx.coroutines.*
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.databinding.ReaderItemBinding
-import xyz.quaver.pupil.hitomi.GalleryInfo
-import xyz.quaver.pupil.ui.ReaderActivity
-import xyz.quaver.pupil.util.downloader.Cache
-import java.io.File
-import kotlin.math.roundToInt
-
-class ReaderAdapter(
- private val activity: ReaderActivity,
- private val galleryID: Int
-) : RecyclerView.Adapter() {
- var galleryInfo: GalleryInfo? = null
-
- var isFullScreen = false
-
- var onItemClickListener : (() -> (Unit))? = null
-
- inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
- init {
- with (binding.image) {
- setImageViewFactory(FrescoImageViewFactory().apply {
- updateView = { imageInfo ->
- binding.image.updateLayoutParams {
- dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
- }
- }
- })
- setImageShownCallback(object : ImageShownCallback {
- override fun onMainImageShown() {
- binding.image.mainView.let { v ->
- when (v) {
- is SubsamplingScaleImageView ->
- if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
- }
- }
- }
-
- override fun onThumbnailShown() {}
- })
-
- setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
- setOnClickListener {
- onItemClickListener?.invoke()
- }
- }
-
- binding.root.setOnClickListener {
- onItemClickListener?.invoke()
- }
- }
-
- fun bind(position: Int) {
- if (cache == null)
- cache = Cache.getInstance(itemView.context, galleryID)
-
- if (!isFullScreen) {
- binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
- binding.image.updateLayoutParams {
- height = 0
- dimensionRatio =
- "${galleryInfo!!.files[position].width}:${galleryInfo!!.files[position].height}"
- }
- } else {
- binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- binding.image.updateLayoutParams {
- height = ConstraintLayout.LayoutParams.MATCH_PARENT
- dimensionRatio = null
- }
- binding.root.background = null
- }
-
- binding.readerIndex.text = (position+1).toString()
-
- val image = cache!!.getImage(position)
- val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
-
- if (progress?.isInfinite() == true && image != null) {
- binding.progressGroup.visibility = View.INVISIBLE
- binding.image.showImage(image.uri)
- } else {
- binding.progressGroup.visibility = View.VISIBLE
- binding.readerItemProgressbar.progress =
- if (progress?.isInfinite() == true)
- 100
- else
- progress?.roundToInt() ?: 0
-
- clear()
-
- CoroutineScope(Dispatchers.Main).launch {
- delay(1000)
- notifyItemChanged(position)
- }
- }
- }
-
- fun clear() {
- binding.image.mainView.let {
- when (it) {
- is SubsamplingScaleImageView ->
- it.recycle()
- is SimpleDraweeView ->
- it.controller = null
- }
- }
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- private var cache: Cache? = null
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(position)
- }
-
- override fun getItemCount() = galleryInfo?.files?.size ?: 0
-
- override fun onViewRecycled(holder: ViewHolder) {
- holder.clear()
- }
-
-}
-
-class FrescoImageViewFactory : ImageViewFactory() {
- var updateView: ((ImageInfo) -> Unit)? = null
-
- override fun createAnimatedImageView(
- context: Context, imageType: Int,
- initScaleType: Int
- ): View {
- val view = SimpleDraweeView(context)
- view.hierarchy.actualImageScaleType = scaleType(initScaleType)
- return view
- }
-
- override fun loadAnimatedContent(
- view: View, imageType: Int,
- imageFile: File
- ) {
- if (view is SimpleDraweeView) {
- val controller: DraweeController = Fresco.newDraweeControllerBuilder()
- .setUri(Uri.parse("file://" + imageFile.absolutePath))
- .setAutoPlayAnimations(true)
- .setControllerListener(object: BaseControllerListener() {
- override fun onIntermediateImageSet(id: String?, imageInfo: ImageInfo?) {
- imageInfo?.let { updateView?.invoke(it) }
- }
-
- override fun onFinalImageSet(id: String?, imageInfo: ImageInfo?, animatable: Animatable?) {
- imageInfo?.let { updateView?.invoke(it) }
- }
- })
- .build()
- view.controller = controller
- }
- }
-
- override fun createThumbnailView(
- context: Context,
- scaleType: ImageView.ScaleType, willLoadFromNetwork: Boolean
- ): View {
- return if (willLoadFromNetwork) {
- val thumbnailView = SimpleDraweeView(context)
- thumbnailView.hierarchy.actualImageScaleType = scaleType(scaleType)
- thumbnailView
- } else {
- super.createThumbnailView(context, scaleType, false)
- }
- }
-
- override fun loadThumbnailContent(view: View, thumbnail: Uri) {
- if (view is SimpleDraweeView) {
- val controller: DraweeController = Fresco.newDraweeControllerBuilder()
- .setUri(thumbnail)
- .build()
- view.controller = controller
- }
- }
-
- private fun scaleType(value: Int): ScalingUtils.ScaleType {
- return when (value) {
- BigImageView.INIT_SCALE_TYPE_CENTER -> ScalingUtils.ScaleType.CENTER
- BigImageView.INIT_SCALE_TYPE_CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
- BigImageView.INIT_SCALE_TYPE_CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
- BigImageView.INIT_SCALE_TYPE_FIT_END -> ScalingUtils.ScaleType.FIT_END
- BigImageView.INIT_SCALE_TYPE_FIT_START -> ScalingUtils.ScaleType.FIT_START
- BigImageView.INIT_SCALE_TYPE_FIT_XY -> ScalingUtils.ScaleType.FIT_XY
- BigImageView.INIT_SCALE_TYPE_FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
- else -> ScalingUtils.ScaleType.FIT_CENTER
- }
- }
-
- private fun scaleType(scaleType: ImageView.ScaleType): ScalingUtils.ScaleType {
- return when (scaleType) {
- ImageView.ScaleType.CENTER -> ScalingUtils.ScaleType.CENTER
- ImageView.ScaleType.CENTER_CROP -> ScalingUtils.ScaleType.CENTER_CROP
- ImageView.ScaleType.CENTER_INSIDE -> ScalingUtils.ScaleType.CENTER_INSIDE
- ImageView.ScaleType.FIT_END -> ScalingUtils.ScaleType.FIT_END
- ImageView.ScaleType.FIT_START -> ScalingUtils.ScaleType.FIT_START
- ImageView.ScaleType.FIT_XY -> ScalingUtils.ScaleType.FIT_XY
- ImageView.ScaleType.FIT_CENTER -> ScalingUtils.ScaleType.FIT_CENTER
- else -> ScalingUtils.ScaleType.FIT_CENTER
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/di/SingletonModule.kt b/app/src/main/java/xyz/quaver/pupil/di/SingletonModule.kt
new file mode 100644
index 00000000..86284f5c
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/di/SingletonModule.kt
@@ -0,0 +1,24 @@
+package xyz.quaver.pupil.di
+
+import android.content.Context
+import com.google.android.datatransport.runtime.dagger.Provides
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import xyz.quaver.pupil.networking.FileImageCache
+import xyz.quaver.pupil.networking.ImageCache
+import java.io.File
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object SingletonModule {
+ @Singleton
+ @Provides
+ fun provideImageCache(
+ @ApplicationContext context: Context
+ ): ImageCache {
+ return FileImageCache(File(context.cacheDir, "image_cache"))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
deleted file mode 100644
index 13d92e0e..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.datetime.Clock.System.now
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.json.Json
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.Request
-import okhttp3.Response
-import xyz.quaver.pupil.client
-import java.io.IOException
-import java.net.URL
-import java.util.concurrent.Executors
-import kotlin.coroutines.resumeWithException
-import kotlin.time.Duration.Companion.minutes
-import kotlin.time.ExperimentalTime
-
-const val protocol = "https:"
-
-@Serializable
-data class Artist(
- val artist: String,
- val url: String
-)
-
-@Serializable
-data class Group(
- val group: String,
- val url: String
-)
-
-@Serializable
-data class Parody(
- val parody: String,
- val url: String
-)
-
-@Serializable
-data class Character(
- val character: String,
- val url: String
-)
-
-@Serializable
-data class Tag(
- val tag: String,
- val url: String,
- val female: String? = null,
- val male: String? = null
-)
-
-@Serializable
-data class Language(
- val galleryid: String,
- val url: String,
- val language_localname: String,
- val name: String
-)
-
-@Serializable
-data class GalleryInfo(
- val id: String,
- val title: String,
- val japanese_title: String? = null,
- val language: String? = null,
- val type: String,
- val date: String,
- val artists: List? = null,
- val groups: List? = null,
- val parodys: List? = null,
- val tags: List? = null,
- val related: List = emptyList(),
- val languages: List = emptyList(),
- val characters: List? = null,
- val scene_indexes: List? = emptyList(),
- val files: List = emptyList()
-)
-
-val json = Json {
- isLenient = true
- ignoreUnknownKeys = true
- allowSpecialFloatingPointValues = true
- useArrayPolymorphism = true
-}
-
-typealias HeaderSetter = (Request.Builder) -> Request.Builder
-fun URL.readText(settings: HeaderSetter? = null): String {
- val request = Request.Builder()
- .url(this).let {
- settings?.invoke(it) ?: it
- }.build()
-
- return client.newCall(request).execute().also{ if (it.code != 200) throw IOException("CODE ${it.code}") }.body?.use { it.string() } ?: throw IOException()
-}
-
-fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
- val request = Request.Builder()
- .url(this).let {
- settings?.invoke(it) ?: it
- }.build()
-
- return client.newCall(request).execute().also { if (it.code != 200) throw IOException("CODE ${it.code}") }.body?.use { it.bytes() } ?: throw IOException()
-}
-
-@Suppress("EXPERIMENTAL_API_USAGE")
-fun getGalleryInfo(galleryID: Int) =
- json.decodeFromString(
- URL("$protocol//$domain/galleries/$galleryID.js").readText()
- .replace("var galleryinfo = ", "")
- )
-
-//common.js
-const val domain = "ltn.hitomi.la"
-const val galleryblockextension = ".html"
-const val galleryblockdir = "galleryblock"
-const val nozomiextension = ".nozomi"
-
-val evaluationContext = Dispatchers.Main + Job()
-
-object gg {
- private var lastRetrieval: Long? = null
-
- private val mutex = Mutex()
-
- private var mDefault = 0
- private val mMap = mutableMapOf()
-
- private var b = ""
-
- @OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class)
- private suspend fun refresh() = withContext(Dispatchers.IO) {
- mutex.withLock {
- if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) {
- val ggjs: String = suspendCancellableCoroutine { continuation ->
- val call = client.newCall(Request.Builder().url("https://ltn.hitomi.la/gg.js").build())
-
- call.enqueue(object: Callback {
- override fun onFailure(call: Call, e: IOException) {
- if (continuation.isCancelled) return
- continuation.resumeWithException(e)
- }
-
- override fun onResponse(call: Call, response: Response) {
- if (!call.isCanceled()) {
- response.body?.use {
- continuation.resume(it.string()) {
- call.cancel()
- }
- }
- }
- }
- })
-
- continuation.invokeOnCancellation {
- call.cancel()
- }
- }
-
- mDefault = Regex("var o = (\\d)").find(ggjs)!!.groupValues[1].toInt()
- val o = Regex("o = (\\d); break;").find(ggjs)!!.groupValues[1].toInt()
-
- mMap.clear()
- Regex("case (\\d+):").findAll(ggjs).forEach {
- val case = it.groupValues[1].toInt()
- mMap[case] = o
- }
-
- b = Regex("b: '(.+)'").find(ggjs)!!.groupValues[1]
-
- lastRetrieval = System.currentTimeMillis()
- }
- }
- }
-
- suspend fun m(g: Int): Int {
- refresh()
-
- return mMap[g] ?: mDefault
- }
-
- suspend fun b(): String {
- refresh()
- return b
- }
- fun s(h: String): String {
- val m = Regex("(..)(.)$").find(h)
- return m!!.groupValues.let { it[2]+it[1] }.toInt(16).toString(10)
- }
-}
-
-suspend fun subdomainFromURL(url: String, base: String? = null) : String {
- var retval = "b"
-
- if (!base.isNullOrBlank())
- retval = base
-
- val b = 16
-
- val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
- val m = r.find(url) ?: return "a"
-
- val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b)
-
- if (g != null) {
- retval = (97+ gg.m(g)).toChar().toString() + retval
- }
-
- return retval
-}
-
-suspend fun urlFromUrl(url: String, base: String? = null) : String {
- return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
-}
-
-suspend fun fullPathFromHash(hash: String) : String =
- "${gg.b()}${gg.s(hash)}/$hash"
-
-fun realFullPathFromHash(hash: String): String =
- hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash")
-
-suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
- val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' }
- val dir = dir ?: "images"
- return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
-}
-
-suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
- if (base == "tn")
- urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base)
- else
- urlFromUrl(urlFromHash(galleryID, image, dir, ext), base)
-
-suspend fun rewriteTnPaths(html: String) {
- html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")) { url ->
- runBlocking {
- urlFromUrl(url.value, "tn")
- }
- }
-}
-
-suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
- return urlFromUrlFromHash(galleryID, image, "webp", null, "a")
-// return when {
-// noWebp ->
-// urlFromUrlFromHash(galleryID, image)
-//// image.hasavif != 0 ->
-//// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
-// image.haswebp != 0 ->
-// urlFromUrlFromHash(galleryID, image, "webp", null, "a")
-// else ->
-// urlFromUrlFromHash(galleryID, image)
-// }
-}
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
deleted file mode 100644
index 6920c666..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleries.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class Gallery(
- val related: List,
- val langList: List>,
- val cover: String,
- val title: String,
- val artists: List,
- val groups: List,
- val type: String,
- val language: String,
- val series: List,
- val characters: List,
- val tags: List,
- val thumbnails: List
-)
-
-suspend fun getGallery(galleryID: Int) : Gallery {
- val info = getGalleryInfo(galleryID)
-
- return Gallery(
- info.related,
- info.languages.map { it.name to it.galleryid },
- urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn"),
- info.title,
- info.artists?.map { it.artist }.orEmpty(),
- info.groups?.map { it.group }.orEmpty(),
- info.type,
- info.language.orEmpty(),
- info.parodys?.map { it.parody }.orEmpty(),
- info.characters?.map { it.character }.orEmpty(),
- info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
- info.files.map { urlFromUrlFromHash(galleryID, it, "webpsmalltn", "webp", "tn") }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
deleted file mode 100644
index dbe19b53..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import kotlinx.serialization.Serializable
-import java.net.URL
-import java.net.URLDecoder
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import javax.net.ssl.HttpsURLConnection
-
-//galleryblock.js
-fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair, Int> {
- val url = when(area) {
- null -> "$protocol//$domain/$tag-$language$nozomiextension"
- else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
- }
-
- with(URL(url).openConnection() as HttpsURLConnection) {
- requestMethod = "GET"
-
- if (start != -1 && count != -1) {
- val startByte = start*4
- val endByte = (start+count)*4-1
-
- setRequestProperty("Range", "bytes=$startByte-$endByte")
- }
-
- connect()
-
- val totalItems = getHeaderField("Content-Range")
- .replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
-
- val nozomi = ArrayList()
-
- val arrayBuffer = ByteBuffer
- .wrap(inputStream.readBytes())
- .order(ByteOrder.BIG_ENDIAN)
-
- while (arrayBuffer.hasRemaining())
- nozomi.add(arrayBuffer.int)
-
- return Pair(nozomi, totalItems)
- }
-}
-
-@Serializable
-data class GalleryBlock(
- val id: Int,
- val galleryUrl: String,
- val thumbnails: List,
- val title: String,
- val artists: List,
- val series: List,
- val type: String,
- val language: String,
- val relatedTags: List,
- val groups: List = emptyList()
-)
-
-suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
- val info = getGalleryInfo(galleryID)
-
- return GalleryBlock(
- galleryID,
- "",
- listOf(urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn")),
- info.title,
- info.artists?.map { it.artist }.orEmpty(),
- info.parodys?.map { it.parody }.orEmpty(),
- info.type,
- info.language.orEmpty(),
- info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
- info.groups?.map { it.group }.orEmpty()
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
deleted file mode 100644
index af8db372..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/reader.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import kotlinx.serialization.Serializable
-import xyz.quaver.pupil.hitomi.GalleryInfo
-import xyz.quaver.pupil.hitomi.getGalleryInfo
-
-@Serializable
-data class GalleryFiles(
- val width: Int,
- val hash: String,
- val haswebp: Int = 0,
- val name: String,
- val height: Int,
- val hasavif: Int = 0,
- val hasavifsmalltn: Int? = 0
-)
-
-//Set header `Referer` to reader url to avoid 403 error
-@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
-fun getReader(galleryID: Int) : GalleryInfo {
- return getGalleryInfo(galleryID)
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
deleted file mode 100644
index 23f78778..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/results.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-import java.util.*
-
-suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set = coroutineScope {
- val terms = query
- .trim()
- .replace(Regex("""^\?"""), "")
- .lowercase()
- .split(Regex("\\s+"))
- .map {
- it.replace('_', ' ')
- }
-
- val positiveTerms = LinkedList()
- val negativeTerms = LinkedList()
-
- for (term in terms) {
- if (term.matches(Regex("^-.+")))
- negativeTerms.push(term.replace(Regex("^-"), ""))
- else if (term.isNotBlank())
- positiveTerms.push(term)
- }
-
- val positiveResults = positiveTerms.map {
- async {
- runCatching {
- getGalleryIDsForQuery(it)
- }.getOrElse { emptySet() }
- }
- }
-
- val negativeResults = negativeTerms.mapIndexed { index, it ->
- async {
- runCatching {
- getGalleryIDsForQuery(it)
- }.getOrElse { emptySet() }
- }
- }
-
- val results = when {
- sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
- positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
- else -> emptySet()
- }.toMutableSet()
-
- fun filterPositive(newResults: Set) {
- when {
- results.isEmpty() -> results.addAll(newResults)
- else -> results.retainAll(newResults)
- }
- }
-
- fun filterNegative(newResults: Set) {
- results.removeAll(newResults)
- }
-
- //positive results
- positiveResults.forEach {
- filterPositive(it.await())
- }
-
- //negative results
- negativeResults.forEachIndexed { index, it ->
- filterNegative(it.await())
- }
-
- results
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
deleted file mode 100644
index ef622bce..00000000
--- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright 2019 tom5079
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package xyz.quaver.pupil.hitomi
-
-import okhttp3.Request
-import xyz.quaver.pupil.client
-import java.net.URL
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import java.security.MessageDigest
-import kotlin.math.min
-
-//searchlib.js
-const val separator = "-"
-const val extension = ".html"
-const val index_dir = "tagindex"
-const val galleries_index_dir = "galleriesindex"
-const val max_node_size = 464
-const val B = 16
-const val compressed_nozomi_prefix = "n"
-
-val tag_index_version: String by lazy { getIndexVersion("tagindex") }
-val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
-
-fun sha256(data: ByteArray) : ByteArray {
- return MessageDigest.getInstance("SHA-256").digest(data)
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-fun hashTerm(term: String) : UByteArray {
- return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
-}
-
-fun sanitize(input: String) : String {
- return input.replace(Regex("[/#]"), "")
-}
-
-fun getIndexVersion(name: String) =
- URL("$protocol//${xyz.quaver.pupil.networking.domain}/$name/version?_=${System.currentTimeMillis()}").readText()
-
-//search.js
-fun getGalleryIDsForQuery(query: String) : Set {
- query.replace("_", " ").let {
- if (it.indexOf(':') > -1) {
- val sides = it.split(":")
- val ns = sides[0]
- var tag = sides[1]
-
- var area : String? = ns
- var language = "all"
- when (ns) {
- "female", "male" -> {
- area = "tag"
- tag = it
- }
- "language" -> {
- area = null
- language = tag
- tag = "index"
- }
- }
-
- return getGalleryIDsFromNozomi(area, tag, language)
- }
-
- val key = hashTerm(it)
- val field = "galleries"
-
- val node = getNodeAtAddress(field, 0) ?: return emptySet()
-
- val data = bSearch(field, key, node)
-
- if (data != null)
- return getGalleryIDsFromData(data)
-
- return emptySet()
- }
-}
-
-fun getSuggestionsForQuery(query: String) : List {
- query.replace('_', ' ').let {
- var field = "global"
- var term = it
-
- if (term.indexOf(':') > -1) {
- val sides = it.split(':')
- field = sides[0]
- term = sides[1]
- }
-
- val key = hashTerm(term)
- val node = getNodeAtAddress(field, 0) ?: return emptyList()
- val data = bSearch(field, key, node)
-
- if (data != null)
- return getSuggestionsFromData(field, data)
-
- return emptyList()
- }
-}
-
-data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
-fun getSuggestionsFromData(field: String, data: Pair) : List {
- val url = "$protocol//${xyz.quaver.pupil.networking.domain}/$index_dir/$field.$tag_index_version.data"
- val (offset, length) = data
- if (length > 10000 || length <= 0)
- throw Exception("length $length is too long")
-
- val inbuf = getURLAtRange(url, offset.until(offset+length))
-
- val suggestions = ArrayList()
-
- val buffer = ByteBuffer
- .wrap(inbuf)
- .order(ByteOrder.BIG_ENDIAN)
- val numberOfSuggestions = buffer.int
-
- if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
- throw Exception("number of suggestions $numberOfSuggestions is too long")
-
- for (i in 0.until(numberOfSuggestions)) {
- var top = buffer.int
-
- val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
- buffer.position(buffer.position()+top)
-
- top = buffer.int
-
- val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
- buffer.position(buffer.position()+top)
-
- val count = buffer.int
-
- val tagname = sanitize(tag)
- val u =
- when(ns) {
- "female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
- "language" -> "/index-$tagname${separator}1$extension"
- else -> "/$ns/$tagname${separator}all${separator}1$extension"
- }
-
- suggestions.add(Suggestion(tag, count, u, ns))
- }
-
- return suggestions
-}
-
-fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set {
- val nozomiAddress =
- when(area) {
- null -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
- else -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
- }
-
- val bytes = try {
- URL(nozomiAddress).readBytes()
- } catch (e: Exception) {
- return emptySet()
- }
-
- val nozomi = mutableSetOf()
-
- val arrayBuffer = ByteBuffer
- .wrap(bytes)
- .order(ByteOrder.BIG_ENDIAN)
-
- while (arrayBuffer.hasRemaining())
- nozomi.add(arrayBuffer.int)
-
- return nozomi
-}
-
-fun getGalleryIDsFromData(data: Pair) : Set {
- val url = "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/galleries.$galleries_index_version.data"
- val (offset, length) = data
- if (length > 100000000 || length <= 0)
- throw Exception("length $length is too long")
-
- val inbuf = getURLAtRange(url, offset.until(offset+length))
-
- val galleryIDs = mutableSetOf()
-
- val buffer = ByteBuffer
- .wrap(inbuf)
- .order(ByteOrder.BIG_ENDIAN)
-
- val numberOfGalleryIDs = buffer.int
-
- val expectedLength = numberOfGalleryIDs*4+4
-
- if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
- throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
- else if (inbuf.size != expectedLength)
- throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
-
- for (i in 0.until(numberOfGalleryIDs))
- galleryIDs.add(buffer.int)
-
- return galleryIDs
-}
-
-fun getNodeAtAddress(field: String, address: Long) : Node? {
- val url =
- when(field) {
- "galleries" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/galleries.$galleries_index_version.index"
- "languages" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/languages.$galleries_index_version.index"
- "nozomiurl" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
- else -> "$protocol//${xyz.quaver.pupil.networking.domain}/$index_dir/$field.$tag_index_version.index"
- }
-
- val nodedata = getURLAtRange(url, address.until(address+ max_node_size))
-
- return decodeNode(nodedata)
-}
-
-fun getURLAtRange(url: String, range: LongRange) : ByteArray {
- val request = Request.Builder()
- .url(url)
- .header("Range", "bytes=${range.first}-${range.last}")
- .build()
-
- return client.newCall(request).execute().body?.use { it.bytes() } ?: byteArrayOf()
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-data class Node(val keys: List, val datas: List>, val subNodeAddresses: List)
-@OptIn(ExperimentalUnsignedTypes::class)
-fun decodeNode(data: ByteArray) : Node {
- val buffer = ByteBuffer
- .wrap(data)
- .order(ByteOrder.BIG_ENDIAN)
-
- val uData = data.toUByteArray()
-
- val numberOfKeys = buffer.int
- val keys = ArrayList()
-
- for (i in 0.until(numberOfKeys)) {
- val keySize = buffer.int
-
- if (keySize == 0 || keySize > 32)
- throw Exception("fatal: !keySize || keySize > 32")
-
- keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
- buffer.position(buffer.position()+keySize)
- }
-
- val numberOfDatas = buffer.int
- val datas = ArrayList>()
-
- for (i in 0.until(numberOfDatas)) {
- val offset = buffer.long
- val length = buffer.int
-
- datas.add(Pair(offset, length))
- }
-
- val numberOfSubNodeAddresses = B +1
- val subNodeAddresses = ArrayList()
-
- for (i in 0.until(numberOfSubNodeAddresses)) {
- val subNodeAddress = buffer.long
- subNodeAddresses.add(subNodeAddress)
- }
-
- return Node(keys, datas, subNodeAddresses)
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-fun bSearch(field: String, key: UByteArray, node: Node) : Pair? {
- fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
- val top = min(dv1.size, dv2.size)
-
- for (i in 0.until(top)) {
- if (dv1[i] < dv2[i])
- return -1
- else if (dv1[i] > dv2[i])
- return 1
- }
-
- return 0
- }
-
- fun locateKey(key: UByteArray, node: Node) : Pair {
- for (i in node.keys.indices) {
- val cmpResult = compareArrayBuffers(key, node.keys[i])
-
- if (cmpResult <= 0)
- return Pair(cmpResult==0, i)
- }
-
- return Pair(false, node.keys.size)
- }
-
- fun isLeaf(node: Node) : Boolean {
- for (subnode in node.subNodeAddresses)
- if (subnode != 0L)
- return false
-
- return true
- }
-
- if (node.keys.isEmpty())
- return null
-
- val (there, where) = locateKey(key, node)
- if (there)
- return node.datas[where]
- else if (isLeaf(node))
- return null
-
- val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null
- return bSearch(field, key, nextNode)
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt b/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt
index e2278294..32a748c8 100644
--- a/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt
+++ b/app/src/main/java/xyz/quaver/pupil/networking/GalleryInfo.kt
@@ -1,5 +1,6 @@
package xyz.quaver.pupil.networking
+import android.os.BaseBundle
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -60,7 +61,29 @@ data class GalleryFile(
val width: Int,
val hash: String,
val name: String,
-)
+) {
+ fun writeToBundle(bundle: BaseBundle) {
+ bundle.putInt("hasWebP", hasWebP)
+ bundle.putInt("hasAVIF", hasAVIF)
+ bundle.putInt("hasJXL", hasJXL)
+ bundle.putInt("height", height)
+ bundle.putInt("width", width)
+ bundle.putString("hash", hash)
+ bundle.putString("name", name)
+ }
+
+ companion object {
+ fun fromBundle(bundle: BaseBundle) = GalleryFile(
+ bundle.getInt("hasWebP"),
+ bundle.getInt("hasAVIF"),
+ bundle.getInt("hasJXL"),
+ bundle.getInt("height"),
+ bundle.getInt("width"),
+ bundle.getString("hash")!!,
+ bundle.getString("name")!!
+ )
+ }
+}
@Serializable
data class GalleryInfo(
@@ -81,10 +104,7 @@ data class GalleryInfo(
val files: List = emptyList()
)
-
@JvmName("joinToCapitalizedStringArtist")
fun List.joinToCapitalizedString() = joinToString { it.artist.replaceFirstChar(Char::titlecase) }
@JvmName("joinToCapitalizedStringGroup")
-fun List.joinToCapitalizedString() = joinToString { it.group.replaceFirstChar(Char::titlecase) }
-@JvmName("joinToCapitalizedStringParody")
-fun List.joinToCapitalizedString() = joinToString { it.series.replaceFirstChar(Char::titlecase) }
\ No newline at end of file
+fun List.joinToCapitalizedString() = joinToString { it.group.replaceFirstChar(Char::titlecase) }
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt
index 2efb6176..835dad68 100644
--- a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt
+++ b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt
@@ -3,10 +3,12 @@ package xyz.quaver.pupil.networking
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.onDownload
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
+import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@@ -16,7 +18,6 @@ import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock.System.now
import kotlinx.datetime.Instant
import kotlinx.serialization.json.Json
-import xyz.quaver.pupil.hitomi.max_node_size
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.IntBuffer
@@ -32,6 +33,7 @@ const val compressedNozomiPrefix = "n"
const val B = 16
const val indexDir = "tagindex"
+const val maxNodeSize = 464
const val galleriesIndexDir = "galleriesindex"
const val languagesIndexDir = "languagesindex"
const val nozomiURLIndexDir = "nozomiurlindex"
@@ -145,7 +147,7 @@ object HitomiHttpClient {
}
return Node.decodeNode(
- getURLAtRange(url, address ..< address+max_node_size)
+ getURLAtRange(url, address ..< address + maxNodeSize)
)
}
@@ -262,36 +264,6 @@ object HitomiHttpClient {
}
}
- suspend fun getImageURL(galleryFile: GalleryFile, thumbnail: Boolean = false): List = buildList {
- val imagePathResolver = imagePathResolver.getValue()
-
- listOf("webp", "avif", "jxl").forEach { type ->
- val available = when {
- thumbnail && type != "jxl" -> true
- type == "webp" -> galleryFile.hasWebP != 0
- type == "avif" -> galleryFile.hasAVIF != 0
- !thumbnail && type == "jxl" -> galleryFile.hasJXL != 0
- else -> false
- }
-
- if (!available) return@forEach
-
- val url = buildString {
- append("https://")
- append(imagePathResolver.decodeSubdomain(galleryFile.hash, thumbnail))
- append(".hitomi.la/")
- append(type)
- if (thumbnail) append("bigtn")
- append('/')
- append(imagePathResolver.decodeImagePath(galleryFile.hash, thumbnail))
- append('.')
- append(type)
- }
-
- add(url)
- }
- }
-
suspend fun search(query: SearchQuery?): Result> = runCatching {
when (query) {
is SearchQuery.Tag -> getGalleryIDsForQuery(query).toSet()
@@ -352,4 +324,50 @@ object HitomiHttpClient {
null -> getGalleryIDsFromNozomi(null, "index", "all").toSet()
}
}
+
+ suspend fun getImageURL(galleryFile: GalleryFile, thumbnail: Boolean = false): List = buildList {
+ val imagePathResolver = imagePathResolver.getValue()
+
+ listOf("webp", "avif", "jxl").forEach { type ->
+ val available = when {
+ thumbnail && type != "jxl" -> true
+ type == "webp" -> galleryFile.hasWebP != 0
+ type == "avif" -> galleryFile.hasAVIF != 0
+ !thumbnail && type == "jxl" -> galleryFile.hasJXL != 0
+ else -> false
+ }
+
+ if (!available) return@forEach
+
+ val url = buildString {
+ append("https://")
+ append(imagePathResolver.decodeSubdomain(galleryFile.hash, thumbnail))
+ append(".hitomi.la/")
+ append(type)
+ if (thumbnail) append("bigtn")
+ append('/')
+ append(imagePathResolver.decodeImagePath(galleryFile.hash, thumbnail))
+ append('.')
+ append(type)
+ }
+
+ add(url)
+ }
+ }
+
+ suspend fun loadImage(
+ galleryFile: GalleryFile,
+ thumbnail: Boolean = false,
+ acceptImage: (String) -> Boolean = { true },
+ onDownload: (bytesSentTotal: Long, contentLength: Long) -> Unit = { _, _ -> }
+ ): Result> {
+ return runCatching {
+ withContext(Dispatchers.IO) {
+ val url = getImageURL(galleryFile, thumbnail).firstOrNull(acceptImage) ?: error("No available image")
+ val channel: ByteReadChannel = httpClient.get(url) { onDownload(onDownload) }.body()
+ Pair(channel, url)
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/networking/ImageCache.kt b/app/src/main/java/xyz/quaver/pupil/networking/ImageCache.kt
new file mode 100644
index 00000000..bcc0b561
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/networking/ImageCache.kt
@@ -0,0 +1,128 @@
+package xyz.quaver.pupil.networking
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import io.ktor.util.cio.writeChannel
+import io.ktor.utils.io.copyAndClose
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import java.io.File
+
+sealed class ImageLoadProgress {
+ data object NotStarted : ImageLoadProgress()
+ data class Progress(val bytesSent: Long, val contentLength: Long) : ImageLoadProgress()
+ data class Finished(val file: File) : ImageLoadProgress()
+ data class Error(val exception: Throwable) : ImageLoadProgress()
+}
+
+interface ImageCache {
+ suspend fun load(galleryFile: GalleryFile, forceDownload: Boolean = false): StateFlow
+ suspend fun free(vararg files: GalleryFile)
+ suspend fun clear()
+}
+
+class FileImageCache(
+ private val cacheDir: File,
+ private val cacheLimit: Long = 128 * 1024 * 1024 // 128MB
+) : ImageCache {
+ private val mutex = Mutex()
+
+ private val requests = mutableMapOf>>()
+ private val activeFiles = mutableMapOf()
+
+ private suspend fun cleanup() = withContext(Dispatchers.IO) {
+ mutex.withLock {
+ val size = cacheDir.listFiles()?.sumOf { it.length() } ?: 0
+
+ if (size > cacheLimit) {
+ cacheDir.listFiles { file ->
+ file.nameWithoutExtension !in activeFiles
+ }?.forEach { file ->
+ file.delete()
+ }
+ }
+ }
+ }
+
+ override suspend fun free(vararg files: GalleryFile) = withContext(Dispatchers.IO) {
+ mutex.withLock {
+ files.forEach { file ->
+ val hash = file.hash
+
+ requests[hash]?.let { (job, _) ->
+ job.cancel()
+ }
+
+ requests.remove(hash)
+ activeFiles.remove(hash)
+ }
+ }
+ }
+
+ override suspend fun clear(): Unit = withContext(Dispatchers.IO) {
+ mutex.withLock {
+ requests.forEach { _, (job, _) -> job.cancel() }
+ activeFiles.clear()
+ cacheDir.deleteRecursively()
+ }
+ }
+
+ override suspend fun load(galleryFile: GalleryFile, forceDownload: Boolean): StateFlow {
+ val hash = galleryFile.hash
+
+ mutex.withLock {
+ val file = activeFiles[hash]
+ if (!forceDownload && file != null) {
+ return MutableStateFlow(ImageLoadProgress.Finished(file))
+ }
+ }
+
+ cleanup()
+
+ mutex.withLock {
+ requests[hash]?.first?.cancelAndJoin()
+ activeFiles[hash]?.delete()
+
+ val flow = MutableStateFlow(ImageLoadProgress.NotStarted)
+ val job = coroutineScope {
+ launch {
+ runCatching {
+ val (channel, url) = HitomiHttpClient.loadImage(galleryFile) { sent, total ->
+ flow.value = ImageLoadProgress.Progress(sent, total)
+ }.onFailure {
+ FirebaseCrashlytics.getInstance().recordException(it)
+ flow.value = ImageLoadProgress.Error(it)
+ }.getOrThrow()
+
+ val file = File(cacheDir, "$hash.${url.substringAfterLast('.')}")
+
+ mutex.withLock {
+ activeFiles.put(hash, file)
+ }
+
+ channel.copyAndClose(file.writeChannel())
+
+ file
+ }.onSuccess { file ->
+ flow.value = ImageLoadProgress.Finished(file)
+ }.onFailure {
+ activeFiles.remove(hash)
+ FirebaseCrashlytics.getInstance().recordException(it)
+ flow.value = ImageLoadProgress.Error(it)
+ }
+ }
+ }
+
+ requests[hash] = job to flow
+
+ return flow
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
deleted file mode 100644
index 77f27cc6..00000000
--- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt
+++ /dev/null
@@ -1,445 +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 .
- */
-
-package xyz.quaver.pupil.services
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ServiceInfo
-import android.os.Build
-import android.util.Log
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.app.TaskStackBuilder
-import androidx.core.content.ContextCompat
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.Response
-import okhttp3.ResponseBody
-import okio.*
-import xyz.quaver.pupil.*
-import xyz.quaver.pupil.ui.ReaderActivity
-import xyz.quaver.pupil.util.*
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import java.io.IOException
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.ceil
-import kotlin.math.log10
-
-private typealias ProgressListener = (DownloadService.Tag, Long, Long, Boolean) -> Unit
-class DownloadService : Service() {
- data class Tag(val galleryID: Int, val index: Int, val startId: Int? = null)
-
- //region Notification
- private val notificationManager by lazy {
- NotificationManagerCompat.from(this)
- }
-
- private val serviceNotification by lazy {
- NotificationCompat.Builder(this, "downloader")
- .setContentTitle(getString(R.string.downloader_running))
- .setProgress(0, 0, false)
- .setSmallIcon(R.drawable.ic_notification)
- .setOngoing(true)
- }
-
- private val notification = ConcurrentHashMap()
-
- private fun initNotification(galleryID: Int) {
- val intent = Intent(this, ReaderActivity::class.java)
- .putExtra("galleryID", galleryID)
-
- val pendingIntent = TaskStackBuilder.create(this).run {
- addNextIntentWithParentStack(intent)
- getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0)
- }
- val action =
- NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
- PendingIntent.getService(
- this,
- R.id.notification_download_cancel_action.normalizeID(),
- Intent(this, DownloadService::class.java)
- .putExtra(KEY_COMMAND, COMMAND_CANCEL)
- .putExtra(KEY_ID, galleryID),
- PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0),
- ).build()
-
- notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
- setContentTitle(getString(R.string.reader_loading))
- setContentText(getString(R.string.reader_notification_text))
- setSmallIcon(R.drawable.ic_notification)
- setContentIntent(pendingIntent)
- addAction(action)
- setProgress(0, 0, true)
- setOngoing(true)
- }
-
- notify(galleryID)
- }
-
- @SuppressLint("RestrictedApi", "MissingPermission")
- private fun notify(galleryID: Int) {
- val max = progress[galleryID]?.size ?: 0
- val progress = progress[galleryID]?.count { it == Float.POSITIVE_INFINITY } ?: 0
-
- val notification = notification[galleryID] ?: return
-
- if (!checkNotificationEnabled(this)) return
-
- if (isCompleted(galleryID)) {
- notification
- .setContentText(getString(R.string.reader_notification_complete))
- .setProgress(0, 0, false)
- .setOngoing(false)
- .mActions.clear()
-
- notificationManager.cancel(galleryID)
- } else
- notification
- .setProgress(max, progress, false)
- .setContentText("$progress/$max")
-
- if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
- notification.let { notificationManager.notify(galleryID, it.build()) }
- else
- notificationManager.cancel(galleryID)
- }
- //endregion
-
- //region ProgressListener
- @Suppress("UNCHECKED_CAST")
- private val progressListener: ProgressListener = { (galleryID, index), bytesRead, contentLength, done ->
- if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
- progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
- }
-
- private class ProgressResponseBody(
- val tag: Any?,
- val responseBody: ResponseBody,
- val progressListener : ProgressListener
- ) : ResponseBody() {
- private var bufferedSource : BufferedSource? = null
-
- override fun contentLength() = responseBody.contentLength()
- override fun contentType() = responseBody.contentType()
-
- override fun source(): BufferedSource {
- if (bufferedSource == null)
- bufferedSource = source(responseBody.source()).buffer()
-
- return bufferedSource!!
- }
-
- private fun source(source: Source) = object: ForwardingSource(source) {
- var totalBytesRead = 0L
-
- override fun read(sink: Buffer, byteCount: Long): Long {
- val bytesRead = super.read(sink, byteCount)
-
- totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
- progressListener.invoke(tag as Tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
-
- return bytesRead
- }
- }
- }
-
- private val interceptor: PupilInterceptor = { chain ->
- val request = chain.request()
-
- var response = kotlin.runCatching {
- chain.proceed(request)
- }.getOrNull()
- var limit = 10
-
- while (response?.isSuccessful != true) {
- if (response?.code == 503) {
- Thread.sleep(200)
- } else if (--limit < 0)
- break
-
- response = kotlin.runCatching {
- chain.proceed(request)
- }.getOrNull()
- }
-
- if (response == null)
- response = chain.proceed(request)
-
- response!!.newBuilder()
- .body(response.body?.let {
- ProgressResponseBody(request.tag(), it, progressListener)
- }).build()
- }
- //endregion
-
- //region Downloader
- /**
- * KEY
- * primary galleryID
- * secondary index
- * PRIMARY VALUE
- * MutableList -> Download in progress
- * null -> Loading / Gallery doesn't exist
- * SECONDARY VALUE
- * 0 <= value < 100 -> Download in progress
- * Float.POSITIVE_INFINITY -> Download completed
- */
- val progress = ConcurrentHashMap>()
- var priority = 0
-
- fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
-
- private val callback = object: Callback {
-
- override fun onFailure(call: Call, e: IOException) {
- Log.d("PUPILD", "ONFAILURE ${call.request().tag()}, ${e}")
- FirebaseCrashlytics.getInstance().recordException(e)
-
- if (e.message?.contains("cancel", true) == false) {
- val galleryID = (call.request().tag() as Tag).galleryID
- }
- }
-
- override fun onResponse(call: Call, response: Response) {
- Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
- val (galleryID, index, startId) = call.request().tag() as Tag
- val ext = call.request().url.encodedPath.split('.').last()
-
- CoroutineScope(Dispatchers.IO).launch {
- runCatching {
- val image = response.also { if (it.code != 200) throw IOException( "$galleryID $index ${response.request.url} CODE ${it.code}" ) }.body?.use { it.bytes() } ?: throw Exception("Response null")
- val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
-
- Cache.getInstance(this@DownloadService, galleryID)
- .putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
-
- progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
- notify(galleryID)
-
- if (isCompleted(galleryID)) {
- if (DownloadManager.getInstance(this@DownloadService)
- .getDownloadFolder(galleryID) != null
- )
- Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
-
- startId?.let { stopSelf(it) }
- }
- }.onFailure {
- FirebaseCrashlytics.getInstance().recordException(it)
- }
- }
- }
- }
-
- fun cancel(startId: Int? = null) {
- client.dispatcher.queuedCalls().filter {
- it.request().tag() is Tag
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
- client.dispatcher.runningCalls().filter {
- it.request().tag() is Tag
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
-
- progress.clear()
- notification.clear()
- notificationManager.cancelAll()
-
- startId?.let { stopSelf(it) }
- }
-
- fun cancel(galleryID: Int, startId: Int? = null) {
- client.dispatcher.queuedCalls().filter {
- (it.request().tag() as? Tag)?.galleryID == galleryID
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
- client.dispatcher.runningCalls().filter {
- (it.request().tag() as? Tag)?.galleryID == galleryID
- }.forEach {
- (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
- it.cancel()
- }
-
- progress.remove(galleryID)
- notification.remove(galleryID)
- notificationManager.cancel(galleryID)
-
- startId?.let { stopSelf(it) }
- }
-
- fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
- cancel(galleryID)
- DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
- Cache.delete(this@DownloadService, galleryID)
-
- startId?.let { stopSelf(it) }
- }
-
- fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
- if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
- return@launch
-
- cleanCache(this@DownloadService)
-
- val cache = Cache.getInstance(this@DownloadService, galleryID)
-
- initNotification(galleryID)
-
- val galleryInfo = cache.getGalleryInfo()
-
- // Gallery doesn't exist
- if (galleryInfo == null) {
- delete(galleryID)
- progress[galleryID] = mutableListOf()
- return@launch
- }
-
- histories.add(galleryID)
-
- progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
-
- cache.metadata.imageList?.let {
- it.forEachIndexed { index, image ->
- progress[galleryID]?.set(index, if (image != null) Float.POSITIVE_INFINITY else 0F)
- }
- }
-
- if (isCompleted(galleryID)) {
- if (DownloadManager.getInstance(this@DownloadService).getDownloadFolder(galleryID) != null)
- Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
-
- notificationManager.cancel(galleryID)
- startId?.let { stopSelf(it) }
- return@launch
- }
-
- notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
- notify(galleryID)
-
- val queued = mutableSetOf()
-
- if (priority) {
- client.dispatcher.queuedCalls().forEach {
- val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
-
- if (queued.add(queuedID))
- cancel(queuedID)
- }
- }
-
- galleryInfo.getRequestBuilders().forEachIndexed { index, it ->
- if (progress[galleryID]?.get(index)?.isInfinite() == false) {
- val request = it.tag(Tag(galleryID, index, startId)).build()
- client.newCall(request).enqueue(callback)
- }
- }
-
- queued.forEach { download(it) }
- }
- //endregion
-
- companion object {
- const val KEY_COMMAND = "COMMAND" // String
- const val KEY_ID = "ID" // Int
- const val KEY_PRIORITY = "PRIORITY" // Boolean
-
- const val COMMAND_DOWNLOAD = "DOWNLOAD"
- const val COMMAND_CANCEL = "CANCEL"
- const val COMMAND_DELETE = "DELETE"
-
- private fun command(context: Context, extras: Intent.() -> Unit) {
- ContextCompat.startForegroundService(context, Intent(context, DownloadService::class.java).apply(extras))
- }
-
- fun download(context: Context, galleryID: Int, priority: Boolean = false) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_DOWNLOAD)
- putExtra(KEY_PRIORITY, priority)
- putExtra(KEY_ID, galleryID)
- }
- }
-
- fun cancel(context: Context, galleryID: Int? = null) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_CANCEL)
- galleryID?.let { putExtra(KEY_ID, it) }
- }
- }
-
- fun delete(context: Context, galleryID: Int) {
- command(context) {
- putExtra(KEY_COMMAND, COMMAND_DELETE)
- putExtra(KEY_ID, galleryID)
- }
- }
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
- startForeground(R.id.downloader_notification_id, serviceNotification.build())
- } else {
- startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
- }
-
- when (intent?.getStringExtra(KEY_COMMAND)) {
- COMMAND_DOWNLOAD -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0)
- download(it, intent.getBooleanExtra(KEY_PRIORITY, false), startId)
- }
- COMMAND_CANCEL -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) cancel(it, startId) else cancel(startId = startId) }
- COMMAND_DELETE -> intent.getIntExtra(KEY_ID, -1).let { if (it > 0) delete(it, startId) }
- }
-
- return START_NOT_STICKY
- }
-
- inner class Binder : android.os.Binder() {
- val service = this@DownloadService
- }
-
- private val binder = Binder()
- override fun onBind(p0: Intent?) = binder
-
- override fun onCreate() {
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
- startForeground(R.id.downloader_notification_id, serviceNotification.build())
- } else {
- startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
- }
- interceptors[Tag::class] = interceptor
- }
-
- override fun onDestroy() {
- interceptors.remove(Tag::class)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/services/ImageCacheService.kt b/app/src/main/java/xyz/quaver/pupil/services/ImageCacheService.kt
new file mode 100644
index 00000000..7893c32a
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/services/ImageCacheService.kt
@@ -0,0 +1,57 @@
+/*
+ * 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 .
+ */
+
+package xyz.quaver.pupil.services
+
+import android.annotation.SuppressLint
+import android.app.job.JobParameters
+import android.app.job.JobService
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import dagger.hilt.android.AndroidEntryPoint
+import io.ktor.util.cio.writeChannel
+import io.ktor.util.collections.ConcurrentMap
+import io.ktor.util.collections.ConcurrentSet
+import io.ktor.utils.io.copyAndClose
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import xyz.quaver.pupil.Pupil
+import xyz.quaver.pupil.networking.GalleryFile
+import xyz.quaver.pupil.networking.HitomiHttpClient
+import java.io.File
+
+@SuppressLint("SpecifyJobSchedulerIdRange")
+@AndroidEntryPoint
+class ImageCacheService : JobService() {
+ override fun onStartJob(params: JobParameters?): Boolean {
+ return false
+ }
+
+ override fun onStopJob(params: JobParameters?): Boolean {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index 37dc5a25..7e32a32c 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -57,7 +57,8 @@ class MainActivity : BaseActivity() {
displayFeatures = displayFeatures,
uiState = uiState,
navController = navController,
- closeDetailScreen = viewModel::closeDetailScreen,
+ openGalleryDetails = viewModel::openGalleryDetails,
+ closeGalleryDetails = viewModel::closeGalleryDetails,
onQueryChange = viewModel::onQueryChange,
loadSearchResult = viewModel::loadSearchResult
)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
deleted file mode 100644
index bfb7c3c9..00000000
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ /dev/null
@@ -1,619 +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 .
- */
-
-package xyz.quaver.pupil.ui
-
-import android.Manifest
-import android.content.ComponentName
-import android.content.Intent
-import android.content.ServiceConnection
-import android.content.pm.PackageManager
-import android.graphics.drawable.Animatable
-import android.graphics.drawable.Drawable
-import android.os.Build
-import android.os.Bundle
-import android.os.IBinder
-import android.view.*
-import android.view.animation.Animation
-import android.view.animation.AnticipateInterpolator
-import android.view.animation.OvershootInterpolator
-import android.view.animation.TranslateAnimation
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
-import androidx.core.content.ContextCompat
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.PagerSnapHelper
-import androidx.recyclerview.widget.RecyclerView
-import androidx.vectordrawable.graphics.drawable.Animatable2Compat
-import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
-import com.google.android.material.snackbar.Snackbar
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import com.google.mlkit.vision.face.Face
-import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.adapters.ReaderAdapter
-import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
-import xyz.quaver.pupil.databinding.ReaderActivityBinding
-import xyz.quaver.pupil.favorites
-import xyz.quaver.pupil.services.DownloadService
-import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.camera
-import xyz.quaver.pupil.util.checkNotificationEnabled
-import xyz.quaver.pupil.util.closeCamera
-import xyz.quaver.pupil.util.downloader.Cache
-import xyz.quaver.pupil.util.downloader.DownloadManager
-import xyz.quaver.pupil.util.requestNotificationPermission
-import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog
-import xyz.quaver.pupil.util.startCamera
-
-class ReaderActivity : BaseActivity() {
-
- private var galleryID = 0
- private var currentPage = 0
-
- private var isScroll = true
- private var isFullscreen = false
- set(value) {
- field = value
-
- (binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
- }
-
- private lateinit var cache: Cache
- var downloader: DownloadService? = null
- private val conn = object: ServiceConnection {
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
- downloader = (service as DownloadService.Binder).service.also {
- it.priority = 0
-
- if (!it.progress.containsKey(galleryID))
- DownloadService.download(this@ReaderActivity, galleryID, true)
- }
- }
-
- override fun onServiceDisconnected(name: ComponentName?) {
- downloader = null
- }
- }
-
- private val snapHelper = PagerSnapHelper()
- private var menu: Menu? = null
-
- private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
- if (isGranted)
- toggleCamera()
- else
- AlertDialog.Builder(this)
- .setTitle(R.string.error)
- .setMessage(R.string.camera_denied)
- .setPositiveButton(android.R.string.ok) { _, _ ->}
- .show()
- }
-
- enum class Eye {
- LEFT,
- RIGHT
- }
-
- private var cameraEnabled = false
- private var eyeType: Eye? = null
- private var eyeTime: Long = 0L
-
- private lateinit var binding: ReaderActivityBinding
-
- private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
- if (!isGranted) {
- showNotificationPermissionExplanationDialog(this)
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ReaderActivityBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- title = getString(R.string.reader_loading)
- supportActionBar?.setDisplayHomeAsUpEnabled(false)
-
- handleIntent(intent)
- cache = Cache.getInstance(this, galleryID)
- FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", galleryID)
-
- if (galleryID == 0) {
- onBackPressed()
- return
- }
-
- initDownloadListener()
- initView()
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- 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) {
- galleryID = if (uri.host?.endsWith("hasha.in") == true) {
- lastPathSegment?.toInt() ?: 0
- } else when (uri.host) {
- "hitomi.la" ->
- Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1)?.toIntOrNull() ?: 0
- "e-hentai.org" -> uri.pathSegments[1].toInt()
- else -> 0
- }
- }
- } else {
- galleryID = intent.getIntExtra("galleryID", 0)
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.reader, menu)
-
- with(menu.findItem(R.id.reader_menu_favorite)) {
- if (favorites.contains(galleryID))
- (icon as Animatable).start()
- }
-
- this.menu = menu
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when(item.itemId) {
- R.id.reader_menu_page_indicator -> {
- // TODO: Switch to DialogFragment
- val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
-
- with(binding.numberPicker) {
- minValue = 1
- maxValue = cache.metadata.galleryInfo?.files?.size ?: 0
- value = currentPage
- }
- val dialog = AlertDialog.Builder(this).apply {
- setView(binding.root)
- }.create()
- binding.okButton.setOnClickListener {
- (this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
- dialog.dismiss()
- }
-
- dialog.show()
- }
- R.id.reader_menu_favorite -> {
- val id = galleryID
- val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
-
- if (favorites.contains(id)) {
- favorites.remove(id)
- favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
- } else {
- favorites.add(id)
- (favorite.icon as Animatable).start()
- }
- }
- }
-
- return true
- }
-
- override fun onResume() {
- super.onResume()
-
- bindService(Intent(this, DownloadService::class.java), conn, BIND_AUTO_CREATE)
-
- if (cameraEnabled)
- startCamera(this, cameraCallback)
- }
-
- override fun onPause() {
- super.onPause()
- closeCamera()
-
- if (downloader != null)
- unbindService(conn)
-
- downloader?.priority = galleryID
- }
-
- override fun onDestroy() {
- super.onDestroy()
-
- update = false
-
- if (!DownloadManager.getInstance(this).isDownloading(galleryID))
- DownloadService.cancel(this, galleryID)
- }
-
- 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 {
- //currentPage is 1-based
- return when(keyCode) {
- KeyEvent.KEYCODE_VOLUME_UP -> {
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
-
- true
- }
- KeyEvent.KEYCODE_VOLUME_DOWN -> {
- (binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
-
- true
- }
- else -> super.onKeyDown(keyCode, event)
- }
- }
-
- private var update = true
- private fun initDownloadListener() {
- CoroutineScope(Dispatchers.Main).launch {
- while (update) {
- delay(1000)
-
- val downloader = downloader ?: continue
-
- if (!downloader.progress.containsKey(galleryID)) //loading
- continue
-
- if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
- update = false
- Snackbar
- .make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
- .show()
-
- return@launch
- }
-
- binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
- binding.downloadProgressbar.progress =
- downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
-
- if (title == getString(R.string.reader_loading)) {
- val galleryInfo = cache.metadata.galleryInfo
-
- if (galleryInfo != null) {
- with(binding.recyclerview.adapter as ReaderAdapter) {
- this.galleryInfo = galleryInfo
- notifyDataSetChanged()
- }
-
- title = galleryInfo.title
- menu?.findItem(R.id.reader_menu_page_indicator)?.title =
- "$currentPage/${galleryInfo.files.size}"
-
- menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi)
- }
- }
-
- if (downloader.isCompleted(galleryID)) { //Download finished
- binding.downloadProgressbar.visibility = View.GONE
-
- animateDownloadFAB(false)
- }
- }
- }
- }
-
- private fun initView() {
- with(binding.recyclerview) {
- adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
- onItemClickListener = {
- if (isScroll) {
- isScroll = false
- isFullscreen = true
-
- scrollMode(false)
- fullscreen(true)
- } else {
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //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()+1
- menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
- }
- })
- }
-
- with(binding.downloadFab) {
- animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
-
- setOnClickListener {
- requestNotificationPermission(
- this@ReaderActivity,
- requestNotificationPermssionLauncher
- ) {
- val downloadManager = DownloadManager.getInstance(this@ReaderActivity)
-
- if (downloadManager.isDownloading(galleryID)) {
- downloadManager.deleteDownloadFolder(galleryID)
- animateDownloadFAB(false)
- } else {
- downloadManager.addDownloadFolder(galleryID)
- DownloadService.download(context, galleryID, true)
- animateDownloadFAB(true)
- }
- }
- }
- }
-
- with(binding.retryFab) {
- setImageResource(R.drawable.refresh)
- setOnClickListener {
- DownloadService.download(context, galleryID)
- }
- }
-
- with(binding.autoFab) {
- setImageResource(R.drawable.eye_white)
- setOnClickListener {
- when {
- ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
- toggleCamera()
- }
- Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
- AlertDialog.Builder(this@ReaderActivity)
- .setTitle(R.string.warning)
- .setMessage(R.string.camera_denied)
- .setPositiveButton(android.R.string.ok) { _, _ ->}
- .show()
- }
- else ->
- requestPermissionLauncher.launch(Manifest.permission.CAMERA)
- }
- }
- }
-
- with(binding.fullscreenFab) {
- setImageResource(R.drawable.ic_fullscreen)
- setOnClickListener {
- isFullscreen = true
- fullscreen(isFullscreen)
-
- binding.fab.close(true)
- }
- }
- }
-
- private fun fullscreen(isFullscreen: Boolean) {
- 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.fill(600)
- }
- }
- }
-
- (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
- }
-
- private fun animateDownloadFAB(animate: Boolean) {
- with(binding.downloadFab) {
- if (animate) {
- val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
-
- icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
- override fun onAnimationEnd(drawable: Drawable?) {
- if (downloader?.isCompleted(galleryID) == true) // If download is finished, stop animating
- post {
- setImageResource(R.drawable.ic_download)
- labelText = getString(R.string.reader_fab_download_cancel)
- }
- else // Or continue animate
- post {
- icon.start()
- labelText = getString(R.string.reader_fab_download_cancel)
- }
- }
- })
-
- setImageDrawable(icon)
- icon?.start()
- } else {
- setImageResource(R.drawable.ic_download)
- labelText = getString(R.string.reader_fab_download)
- }
- }
- }
-
- val cameraCallback: (List) -> Unit = callback@{ faces ->
- binding.eyeCard.dot.let {
- it.visibility = View.VISIBLE
- CoroutineScope(Dispatchers.Main).launch {
- delay(50)
- it.visibility = View.INVISIBLE
- }
- }
-
- if (faces.size != 1)
- ContextCompat.getDrawable(this, R.drawable.eye_off).let {
- with(binding.eyeCard) {
- leftEye.setImageDrawable(it)
- rightEye.setImageDrawable(it)
- }
-
- return@callback
- }
-
- val (left, right) = Pair(
- faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true,
- faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
- )
-
- with(binding.eyeCard) {
- leftEye.setImageDrawable(
- ContextCompat.getDrawable(
- leftEye.context,
- if (left) R.drawable.eye else R.drawable.eye_closed
- )
- )
- rightEye.setImageDrawable(
- ContextCompat.getDrawable(
- rightEye.context,
- if (right) R.drawable.eye else R.drawable.eye_closed
- )
- )
- }
-
- when {
- // Both closed / opened
- !left.xor(right) -> {
- eyeType = null
- eyeTime = 0L
- }
- !left -> {
- if (eyeType != Eye.LEFT) {
- eyeType = Eye.LEFT
- eyeTime = System.currentTimeMillis()
- }
- }
- !right -> {
- if (eyeType != Eye.RIGHT) {
- eyeType = Eye.RIGHT
- eyeTime = System.currentTimeMillis()
- }
- }
- }
-
- if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
- (binding.recyclerview.layoutManager as LinearLayoutManager).let {
- it.scrollToPositionWithOffset(when(eyeType!!) {
- Eye.RIGHT -> {
- if (it.reverseLayout) currentPage - 2 else currentPage
- }
- Eye.LEFT -> {
- if (it.reverseLayout) currentPage else currentPage - 2
- }
- }, 0)
- }
-
- eyeTime = System.currentTimeMillis() + 500
- }
- }
-
- private fun toggleCamera() {
- val eyes = binding.eyeCard.root
- when (camera) {
- null -> {
- binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
- binding.autoFab.setImageResource(R.drawable.eye_off_white)
- eyes.apply {
- visibility = View.VISIBLE
- TranslateAnimation(0F, 0F, -100F, 0F).apply {
- duration = 500
- fillAfter = false
- interpolator = OvershootInterpolator()
- }.let { startAnimation(it) }
- }
- startCamera(this, cameraCallback)
- cameraEnabled = true
- }
- else -> {
- binding.autoFab.labelText = getString(R.string.reader_fab_auto)
- binding.autoFab.setImageResource(R.drawable.eye_white)
- eyes.apply {
- TranslateAnimation(0F, 0F, 0F, -100F).apply {
- duration = 500
- fillAfter = false
- interpolator = AnticipateInterpolator()
- setAnimationListener(object: Animation.AnimationListener {
- override fun onAnimationStart(p0: Animation?) {}
- override fun onAnimationRepeat(p0: Animation?) {}
-
- override fun onAnimationEnd(p0: Animation?) {
- eyes.visibility = View.GONE
- }
- })
- }.let { startAnimation(it) }
- }
- closeCamera()
- cameraEnabled = false
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/Gallery.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/Gallery.kt
index 2ae4754c..51bb3886 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/Gallery.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/Gallery.kt
@@ -1,6 +1,7 @@
package xyz.quaver.pupil.ui.composable
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -9,13 +10,18 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.QuestionMark
import androidx.compose.material.icons.filled.StarOutline
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
@@ -33,12 +39,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.Placeholder
+import androidx.compose.ui.text.PlaceholderVerticalAlign
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import xyz.quaver.pupil.R
@@ -50,8 +62,13 @@ import xyz.quaver.pupil.networking.GalleryTag
import xyz.quaver.pupil.networking.Group
import xyz.quaver.pupil.networking.HitomiHttpClient
import xyz.quaver.pupil.networking.Language
+import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.networking.Series
import xyz.quaver.pupil.networking.joinToCapitalizedString
+import xyz.quaver.pupil.ui.theme.Blue500
+import xyz.quaver.pupil.ui.theme.Green500
+import xyz.quaver.pupil.ui.theme.Purple500
+import xyz.quaver.pupil.ui.theme.Red500
import xyz.quaver.pupil.ui.theme.Yellow500
private val languageMap = mapOf(
@@ -94,6 +111,22 @@ private val languageMap = mapOf(
"japanese" to "日本語"
)
+private val galleryTypeStringMap = mapOf(
+ "doujinshi" to R.string.doujinshi,
+ "manga" to R.string.manga,
+ "artistcg" to R.string.artist_cg,
+ "gamecg" to R.string.game_cg,
+ "imageset" to R.string.image_set
+)
+
+private val galleryTypeColorMap = mapOf(
+ "doujinshi" to Red500,
+ "manga" to Yellow500,
+ "artistcg" to Purple500,
+ "gamecg" to Green500,
+ "imageset" to Blue500
+)
+
class GalleryInfoProvider: PreviewParameterProvider {
override val values = sequenceOf(
GalleryInfo(
@@ -211,18 +244,19 @@ class GalleryInfoProvider: PreviewParameterProvider {
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun TagGroup(tags: List) {
- var isFolded by remember { mutableStateOf(true) }
+fun TagGroup(tags: List, folded: Boolean = false) {
+ var isFolded by remember { mutableStateOf(folded) }
FlowRow(
- Modifier.padding(0.dp, 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
tags.sortedBy {
- if (!it.female.isNullOrEmpty()) 1
- else if (!it.female.isNullOrEmpty()) 2
- else 3
+ when(it.namespace) {
+ "female" -> 1
+ "male" -> 2
+ else -> 3
+ }
}.let {
if (isFolded) it.take(10) else it
}.forEach { tag ->
@@ -246,6 +280,59 @@ fun TagGroup(tags: List) {
}
}
+@Composable
+fun GalleryTypeIndicator(galleryType: String) {
+ Surface(
+ modifier = Modifier.height(32.dp),
+ color = galleryTypeColorMap[galleryType] ?: MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Box(Modifier.fillMaxHeight()) {
+ Text(
+ galleryTypeStringMap[galleryType]?.let { stringResource(it) } ?: galleryType,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .align(Alignment.Center),
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.White
+ )
+ }
+ }
+}
+
+@Composable
+fun LanguageTitle(title: String, language: String?) {
+ val icon = languageIconMap[language]
+
+ if (icon != null) {
+ Text(
+ buildAnnotatedString {
+ appendInlineContent("language", "")
+ append(' ')
+ append(title)
+ },
+ style = MaterialTheme.typography.headlineSmall,
+ inlineContent = mapOf(
+ "language" to InlineTextContent(
+ Placeholder(
+ width = 20.sp,
+ height = 20.sp,
+ placeholderVerticalAlign = PlaceholderVerticalAlign.Center
+ )
+ ) {
+ Icon(
+ painterResource(icon),
+ contentDescription = null,
+ tint = Color.Unspecified
+ )
+ }
+ )
+ )
+ } else {
+ Text(title, style = MaterialTheme.typography.headlineSmall)
+ }
+}
+
@Composable
fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
val thumbnailFile = galleryInfo.files.first()
@@ -263,7 +350,7 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
.fillMaxWidth()
.aspectRatio(aspectRatio)
.clip(RoundedCornerShape(8.dp)),
- loading = { CircularProgressIndicator(Modifier.size(32.dp)) },
+ loading = { CircularProgressIndicator(Modifier.align(Alignment.Center)) },
error = {
Image(
painter = painterResource(R.drawable.thumbnail),
@@ -273,11 +360,18 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
contentDescription = "Thumbnail"
)
} else {
- Box(Modifier.fillMaxWidth().aspectRatio(aspectRatio)) {
- CircularProgressIndicator(Modifier.size(32.dp))
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .aspectRatio(aspectRatio)) {
+ CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
- Text(galleryInfo.title, style = MaterialTheme.typography.headlineSmall)
+
+ Spacer(Modifier.height(8.dp))
+
+ LanguageTitle(galleryInfo.title, galleryInfo.language)
+
val artistsAndGroups = buildString {
if (!galleryInfo.artists.isNullOrEmpty())
append(galleryInfo.artists.joinToCapitalizedString())
@@ -290,34 +384,16 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
}
}
- Text(
- artistsAndGroups,
- style = MaterialTheme.typography.labelLarge
- )
-
- Spacer(Modifier.height(8.dp))
-
- if (galleryInfo.series?.isNotEmpty() == true)
+ if (artistsAndGroups.isNotEmpty()) {
Text(
- "Series: ${galleryInfo.series.joinToCapitalizedString()}",
- style = MaterialTheme.typography.bodyMedium
- )
-
- Text(
- "Type: ${galleryInfo.type}",
- style = MaterialTheme.typography.bodyMedium
- )
-
- languageMap[galleryInfo.language]?.let {
- Text(
- "Language: $it",
- style = MaterialTheme.typography.bodyMedium
+ artistsAndGroups,
+ style = MaterialTheme.typography.labelLarge,
)
}
}
} else {
Row(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (thumbnailUrl != null) {
@@ -330,7 +406,7 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
.height(200.dp)
.aspectRatio(aspectRatio)
.clip(RoundedCornerShape(8.dp)),
- loading = { CircularProgressIndicator(Modifier.size(32.dp)) },
+ loading = { CircularProgressIndicator(Modifier.align(Alignment.Center)) },
error = {
Image(
painter = painterResource(R.drawable.thumbnail),
@@ -340,12 +416,16 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
contentDescription = "Thumbnail"
)
} else {
- Box(Modifier.height(200.dp).aspectRatio(aspectRatio)) {
- CircularProgressIndicator(Modifier.size(32.dp))
+ Box(
+ Modifier
+ .height(200.dp)
+ .aspectRatio(aspectRatio)) {
+ CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
Column(Modifier.heightIn(min = 200.dp)) {
- Text(galleryInfo.title, style = MaterialTheme.typography.headlineSmall)
+ LanguageTitle(galleryInfo.title, galleryInfo.language)
+
val artistsAndGroups = buildString {
if (!galleryInfo.artists.isNullOrEmpty())
append(galleryInfo.artists.joinToCapitalizedString())
@@ -358,31 +438,10 @@ fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
}
}
- Text(
- artistsAndGroups,
- style = MaterialTheme.typography.labelLarge
- )
-
- Spacer(
- Modifier
- .weight(1f)
- .heightIn(min = 8.dp))
-
- if (galleryInfo.series?.isNotEmpty() == true)
+ if (artistsAndGroups.isNotEmpty()) {
Text(
- "Series: ${galleryInfo.series.joinToCapitalizedString()}",
- style = MaterialTheme.typography.bodyMedium
- )
-
- Text(
- "Type: ${galleryInfo.type}",
- style = MaterialTheme.typography.bodyMedium
- )
-
- languageMap[galleryInfo.language]?.let {
- Text(
- "Language: $it",
- style = MaterialTheme.typography.bodyMedium
+ artistsAndGroups,
+ style = MaterialTheme.typography.labelLarge
)
}
}
@@ -405,14 +464,16 @@ fun DetailedGalleryInfo(
}
Card(modifier) {
- Column(Modifier.padding(8.dp)) {
+ Column(Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
DetailedGalleryInfoHeader(galleryInfo, thumbnailUrl)
+ GalleryTypeIndicator(galleryInfo.type)
+
if (galleryInfo.tags?.isNotEmpty() == true) {
- TagGroup(galleryInfo.tags)
+ TagGroup(galleryInfo.tags.map { it.toTag() }, folded = true)
}
- HorizontalDivider(Modifier.padding(4.dp))
+ HorizontalDivider()
Box(
Modifier
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt
index 2d752c99..0878b345 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt
@@ -2,8 +2,6 @@ package xyz.quaver.pupil.ui.composable
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -40,8 +38,8 @@ import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
+import xyz.quaver.pupil.networking.GalleryInfo
import xyz.quaver.pupil.networking.SearchQuery
-import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.viewmodel.SearchState
@@ -51,9 +49,10 @@ fun MainApp(
displayFeatures: List,
uiState: SearchState,
navController: NavHostController,
- closeDetailScreen: () -> Unit,
+ openGalleryDetails: (GalleryInfo) -> Unit,
+ closeGalleryDetails: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
- loadSearchResult: (IntRange) -> Unit
+ loadSearchResult: (IntRange) -> Unit,
) {
val navigationType: NavigationType
val contentType: ContentType
@@ -106,7 +105,8 @@ fun MainApp(
navigationContentPosition,
uiState,
navController,
- closeDetailScreen = closeDetailScreen,
+ openGalleryDetails = openGalleryDetails,
+ closeGalleryDetails = closeGalleryDetails,
onQueryChange = onQueryChange,
loadSearchResult = loadSearchResult
)
@@ -120,7 +120,8 @@ private fun MainNavigationWrapper(
navigationContentPosition: NavigationContentPosition,
uiState: SearchState,
navController: NavHostController,
- closeDetailScreen: () -> Unit,
+ openGalleryDetails: (GalleryInfo) -> Unit,
+ closeGalleryDetails: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
loadSearchResult: (IntRange) -> Unit
) {
@@ -156,9 +157,10 @@ private fun MainNavigationWrapper(
uiState = uiState,
navController = navController,
onDrawerClicked = openDrawer,
- closeDetailScreen = closeDetailScreen,
+ openGalleryDetails = openGalleryDetails,
+ closeGalleryDetails = closeGalleryDetails,
onQueryChange = onQueryChange,
- loadSearchResult = loadSearchResult
+ loadSearchResult = loadSearchResult,
)
}
} else {
@@ -187,9 +189,10 @@ private fun MainNavigationWrapper(
uiState = uiState,
navController = navController,
onDrawerClicked = openDrawer,
- closeDetailScreen = closeDetailScreen,
+ openGalleryDetails = openGalleryDetails,
+ closeGalleryDetails = closeGalleryDetails,
onQueryChange = onQueryChange,
- loadSearchResult = loadSearchResult
+ loadSearchResult = loadSearchResult,
)
}
}
@@ -217,9 +220,10 @@ fun MainContent(
uiState: SearchState,
navController: NavHostController,
onDrawerClicked: () -> Unit,
- closeDetailScreen: () -> Unit,
+ openGalleryDetails: (GalleryInfo) -> Unit,
+ closeGalleryDetails: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
- loadSearchResult: (IntRange) -> Unit
+ loadSearchResult: (IntRange) -> Unit,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
@@ -261,9 +265,16 @@ fun MainContent(
contentType = contentType,
displayFeatures = displayFeatures,
uiState = uiState,
- closeDetailScreen = closeDetailScreen,
+ openGalleryDetails = openGalleryDetails,
+ closeGalleryDetails = closeGalleryDetails,
onQueryChange = onQueryChange,
- loadSearchResult = loadSearchResult
+ loadSearchResult = loadSearchResult,
+ openGallery = {
+ Log.d("PUPILD", "openGallery: ${it.id}")
+ navController.navigate(MainDestination.ImageViewer(it.id).route) {
+ launchSingleTop = true
+ }
+ }
)
}
composable(MainDestination.History.route) {
@@ -278,8 +289,8 @@ fun MainContent(
activity(MainDestination.Settings.route) {
activityClass = SettingsActivity::class
}
- activity(MainDestination.ImageViewer.route) {
- activityClass = ReaderActivity::class
+ activity(MainDestination.ImageViewer.commonRoute) {
+// argument("galleryID") { type = NavType.IntType }
}
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt
index 1446d0da..a4c6c666 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt
@@ -47,11 +47,14 @@ sealed interface MainDestination {
override val textId = R.string.main_destination_settings
}
- data object ImageViewer: MainDestination {
- override val route = "image_viewer"
+ class ImageViewer(galleryID: String): MainDestination {
+ override val route = "image_viewer/$galleryID"
override val icon = Icons.AutoMirrored.Filled.MenuBook
override val textId = R.string.main_destination_image_viewer
+ companion object {
+ val commonRoute = "image_viewer/{galleryID}"
+ }
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/SearchScreen.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/SearchScreen.kt
index cc1cc583..2af11d12 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/SearchScreen.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/SearchScreen.kt
@@ -35,27 +35,30 @@ import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Female
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Group
-import androidx.compose.material.icons.filled.LocalOffer
import androidx.compose.material.icons.filled.Male
import androidx.compose.material.icons.filled.Translate
+import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@@ -92,6 +95,7 @@ import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
import xyz.quaver.pupil.R
import xyz.quaver.pupil.networking.GalleryInfo
+import xyz.quaver.pupil.networking.HitomiHttpClient
import xyz.quaver.pupil.networking.SearchQuery
import xyz.quaver.pupil.ui.theme.Blue600
import xyz.quaver.pupil.ui.theme.Pink600
@@ -108,10 +112,10 @@ private val iconMap = mapOf(
"series" to Icons.Default.Book,
"type" to Icons.Default.Folder,
"language" to Icons.Default.Translate,
- "tag" to Icons.Default.LocalOffer,
+ "tag" to Icons.AutoMirrored.Filled.Label,
)
-private val languageMap = mapOf(
+val languageIconMap = mapOf(
"indonesian" to R.drawable.language_indonesian,
"javanese" to R.drawable.language_javanese,
"catalan" to R.drawable.language_catalan,
@@ -155,9 +159,9 @@ fun TagChipIcon(tag: SearchQuery.Tag) {
val icon = iconMap[tag.namespace]
if (icon != null) {
- if (tag.namespace == "language" && languageMap.contains(tag.tag)) {
+ if (tag.namespace == "language" && languageIconMap.contains(tag.tag)) {
Icon(
- painter = painterResource(languageMap[tag.tag]!!),
+ painter = painterResource(languageIconMap[tag.tag]!!),
contentDescription = "icon",
modifier = Modifier
.padding(4.dp)
@@ -360,7 +364,8 @@ fun SearchBar(
indication = null
) {
focused = true
- }.onGloballyPositioned {
+ }
+ .onGloballyPositioned {
onSearchBarPositioned(it.positionInRoot().y.roundToInt() + it.size.height)
}
.absoluteOffset { IntOffset(0, topOffset) },
@@ -414,6 +419,7 @@ fun GalleryList(
error: Boolean = false,
onPageChange: (Int) -> Unit,
onQueryChange: (SearchQuery?) -> Unit = {},
+ openGalleryDetails: (GalleryInfo) -> Unit,
) {
val listState = rememberLazyListState()
var topOffset by remember { mutableIntStateOf(0) }
@@ -480,7 +486,8 @@ fun GalleryList(
DetailedGalleryInfo(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 4.dp),
+ .padding(horizontal = 4.dp)
+ .clickable { openGalleryDetails(galleryInfo) },
galleryInfo = galleryInfo
)
}
@@ -490,21 +497,76 @@ fun GalleryList(
}
}
+@Composable
+fun DetailScreen(
+ galleryInfo: GalleryInfo,
+ closeGalleryDetails: () -> Unit = { },
+ openGallery: (GalleryInfo) -> Unit = { }
+) {
+ var thumbnailUrl by remember { mutableStateOf(null) }
+
+ LaunchedEffect(galleryInfo) {
+ thumbnailUrl = galleryInfo.files.firstOrNull()?.let {
+ HitomiHttpClient.getImageURL(it, true).firstOrNull()
+ } ?: ""
+ }
+
+ Column(
+ Modifier
+ .padding(8.dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
+ IconButton(onClick = closeGalleryDetails) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close Detail")
+ }
+
+ DetailedGalleryInfoHeader(galleryInfo, thumbnailUrl)
+
+ Row(Modifier.fillMaxWidth()) {
+ FilledTonalButton(
+ modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
+ onClick = { /*TODO*/ }
+ ) {
+ Text(stringResource(R.string.download))
+ }
+
+ Button(
+ modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
+ onClick = { openGallery(galleryInfo) }
+ ) {
+ Text("Open")
+ }
+ }
+
+ GalleryTypeIndicator(galleryInfo.type)
+
+ if (galleryInfo.series?.isNotEmpty() == true) {
+ TagGroup(galleryInfo.series.map { it.toTag() })
+ }
+
+ if (galleryInfo.characters?.isNotEmpty() == true) {
+ TagGroup(galleryInfo.characters.map { it.toTag() })
+ }
+
+ if (galleryInfo.tags?.isNotEmpty() == true) {
+ TagGroup(galleryInfo.tags.map { it.toTag() })
+ }
+ }
+}
+
@Composable
fun SearchScreen(
contentType: ContentType,
displayFeatures: List,
uiState: SearchState,
- closeDetailScreen: () -> Unit,
+ openGalleryDetails: (GalleryInfo) -> Unit,
+ closeGalleryDetails: () -> Unit,
onQueryChange: (SearchQuery?) -> Unit,
- loadSearchResult: (IntRange) -> Unit
+ loadSearchResult: (IntRange) -> Unit,
+ openGallery: (GalleryInfo) -> Unit
) {
- LaunchedEffect(contentType) {
- if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
- closeDetailScreen()
- }
- }
-
val itemsPerPage by remember { mutableIntStateOf(20) }
val pageToRange: (Int) -> IntRange = remember(itemsPerPage) {{ page ->
@@ -527,7 +589,19 @@ fun SearchScreen(
loadSearchResult(pageToRange(page))
}}
- LaunchedEffect(uiState.query, currentPage) { loadSearchResult(pageToRange(currentPage)) }
+ LaunchedEffect(uiState.query) { loadSearchResult(pageToRange(currentPage)) }
+
+ LaunchedEffect(contentType) {
+ if (contentType == ContentType.SINGLE_PANE && !uiState.isDetailOnlyOpen) {
+ closeGalleryDetails()
+ }
+ }
+
+ if (contentType == ContentType.SINGLE_PANE && uiState.isDetailOnlyOpen) {
+ BackHandler {
+ closeGalleryDetails()
+ }
+ }
if (contentType == ContentType.DUAL_PANE) {
TwoPane(
@@ -541,7 +615,8 @@ fun SearchScreen(
loading = uiState.loading,
error = uiState.error,
onQueryChange = onQueryChange,
- onPageChange = loadResult
+ onPageChange = loadResult,
+ openGalleryDetails = openGalleryDetails
)
},
second = {
@@ -551,17 +626,30 @@ fun SearchScreen(
displayFeatures = displayFeatures
)
} else {
- GalleryList(
- contentType = contentType,
- galleries = uiState.galleries,
- query = uiState.query,
- currentPage = currentPage,
- maxPage = maxPage,
- loading = uiState.loading,
- error = uiState.error,
- onQueryChange = onQueryChange,
- onPageChange = loadResult
- )
+ val detailGallery = uiState.openedGallery
+ AnimatedVisibility(!uiState.isDetailOnlyOpen || detailGallery == null) {
+ GalleryList(
+ contentType = contentType,
+ galleries = uiState.galleries,
+ query = uiState.query,
+ currentPage = currentPage,
+ maxPage = maxPage,
+ loading = uiState.loading,
+ error = uiState.error,
+ onQueryChange = onQueryChange,
+ onPageChange = loadResult,
+ openGalleryDetails = openGalleryDetails
+ )
+ }
+ AnimatedVisibility(uiState.isDetailOnlyOpen && detailGallery != null) {
+ if (detailGallery != null) {
+ DetailScreen(
+ galleryInfo = detailGallery,
+ closeGalleryDetails = closeGalleryDetails,
+ openGallery = openGallery
+ )
+ }
+ }
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/reader/ImageViewer.kt b/app/src/main/java/xyz/quaver/pupil/ui/reader/ImageViewer.kt
new file mode 100644
index 00000000..b7a8a540
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/ui/reader/ImageViewer.kt
@@ -0,0 +1,2 @@
+package xyz.quaver.pupil.ui.reader
+
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
index af277092..67e0933b 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
@@ -1,5 +1,6 @@
package xyz.quaver.pupil.ui.viewmodel
+import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
@@ -18,7 +19,14 @@ class MainViewModel : ViewModel() {
private var searchSource: GallerySearchSource = GallerySearchSource(null)
private var job: Job? = null
- fun closeDetailScreen() {
+ fun openGalleryDetails(galleryInfo: GalleryInfo) {
+ _uiState.value = _uiState.value.copy(
+ openedGallery = galleryInfo,
+ isDetailOnlyOpen = true
+ )
+ }
+
+ fun closeGalleryDetails() {
_uiState.value = _uiState.value.copy(
isDetailOnlyOpen = false
)
@@ -35,11 +43,13 @@ class MainViewModel : ViewModel() {
}
fun loadSearchResult(range: IntRange) {
+ Thread.dumpStack()
job?.cancel()
job = viewModelScope.launch {
val sanitizedRange = max(range.first, 0) .. min(range.last, searchState.value.galleryCount ?: Int.MAX_VALUE)
_uiState.value = _uiState.value.copy(
loading = true,
+ error = false,
currentRange = sanitizedRange
)
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
deleted file mode 100644
index 0bcba433..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt
+++ /dev/null
@@ -1,297 +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 .
- */
-
-package xyz.quaver.pupil.util.downloader
-
-import android.content.Context
-import android.content.ContextWrapper
-import android.net.Uri
-import kotlinx.coroutines.*
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-import okhttp3.Request
-import xyz.quaver.io.FileX
-import xyz.quaver.io.util.*
-import xyz.quaver.pupil.client
-import xyz.quaver.pupil.hitomi.*
-import java.io.File
-import java.io.IOException
-import java.util.concurrent.ConcurrentHashMap
-
-@Serializable
-data class OldReader(
- val code: String,
- val galleryInfo: OldGalleryInfo
-)
-
-@Serializable
-data class OldGalleryInfo(
- val language_localname: String? = null,
- val language: String? = null,
- val date: String? = null,
- val files: List,
- val id: Int? = null,
- val type: String? = null,
- val title: String? = null
-)
-
-@Serializable
-data class OldGalleryFiles(
- val width: Int,
- val hash: String,
- val haswebp: Int = 0,
- val name: String,
- val height: Int,
- val hasavif: Int = 0,
- val hasavifsmalltn: Int? = 0
-)
-
-@Serializable
-data class OldMetadata(
- var galleryBlock: GalleryBlock? = null,
- var reader: OldReader? = null,
- var imageList: MutableList? = null
-) {
- fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
-}
-
-@Serializable
-data class Metadata(
- var galleryBlock: GalleryBlock? = null,
- var galleryInfo: GalleryInfo? = null,
- var imageList: MutableList? = null
-) {
- constructor(old: OldMetadata) : this(
- old.galleryBlock,
- old.reader?.galleryInfo?.let { oldGalleryInfo ->
- GalleryInfo(
- oldGalleryInfo.id.toString(),
- oldGalleryInfo.title ?: "",
- null,
- oldGalleryInfo.language,
- oldGalleryInfo.type ?: "",
- oldGalleryInfo.date ?: "",
- files = oldGalleryInfo.files.map {
- GalleryFiles(
- it.width,
- it.hash,
- it.haswebp,
- it.name,
- it.height,
- it.hasavif,
- it.hasavifsmalltn
- )
- }
- )
- },
- old.imageList
- )
-
- fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
-}
-
-class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
- companion object {
- val instances = ConcurrentHashMap()
-
- fun getInstance(context: Context, galleryID: Int) =
- instances[galleryID] ?: synchronized(this) {
- instances[galleryID] ?: Cache(context, galleryID).also { instances.put(galleryID, it) }
- }
-
- @Synchronized
- fun delete(context: Context, galleryID: Int) {
- File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
- instances.remove(galleryID)
- }
- }
-
- init {
- cacheFolder.mkdirs()
- }
-
- var metadata = kotlin.runCatching {
- findFile(".metadata")?.readText()?.let { metadata ->
- kotlin.runCatching {
- Json.decodeFromString(metadata)
- }.getOrElse {
- Metadata(json.decodeFromString(metadata))
- }
- }
- }.onFailure { it.printStackTrace() }.getOrNull() ?: Metadata()
-
- val downloadFolder: FileX?
- get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
-
- val cacheFolder: FileX
- get() = FileX(this, cacheDir, "imageCache/$galleryID").also {
- if (!it.exists())
- it.mkdirs()
- }
-
- fun findFile(fileName: String): FileX? =
- downloadFolder?.let { downloadFolder -> downloadFolder.getChild(fileName).let {
- if (it.exists()) it else null
- } } ?: cacheFolder.getChild(fileName).let {
- if (it.exists()) it else null
- }
-
- @Suppress("BlockingMethodInNonBlockingContext")
- fun setMetadata(change: (Metadata) -> Unit) {
- change.invoke(metadata)
-
- val file = cacheFolder.getChild(".metadata")
-
- kotlin.runCatching {
- if (!file.exists()) {
- file.createNewFile()
- }
- file.writeText(Json.encodeToString(metadata))
- }
- }
-
- suspend fun getGalleryBlock(): GalleryBlock? {
- return metadata.galleryBlock
- ?: withContext(Dispatchers.IO) {
- try {
- getGalleryBlock(galleryID).also {
- setMetadata { metadata -> metadata.galleryBlock = it }
- }
- } catch (e: Exception) { return@withContext null }
- }
- }
-
- @Suppress("BlockingMethodInNonBlockingContext")
- suspend fun getThumbnail(): Uri =
- findFile(".thumbnail")?.uri
- ?: getGalleryBlock()?.thumbnails?.firstOrNull()?.let { withContext(Dispatchers.IO) {
- kotlin.runCatching {
- val request = Request.Builder()
- .url(it)
- .header("Referer", "https://hitomi.la/")
- .build()
-
- client.newCall(request).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.bytes() }
- }.getOrNull()?.let { thumbnail -> kotlin.runCatching {
- cacheFolder.getChild(".thumbnail").also {
- if (!it.exists())
- it.createNewFile()
-
- it.writeBytes(thumbnail)
- }
- }.getOrNull()?.uri }
- } } ?: Uri.EMPTY
-
- suspend fun getGalleryInfo(): GalleryInfo? {
-
- return metadata.galleryInfo
- ?: withContext(Dispatchers.IO) {
- try {
- getGalleryInfo(galleryID).also {
- setMetadata { metadata ->
- metadata.galleryInfo = it
-
- if (metadata.imageList == null)
- metadata.imageList = MutableList(it.files.size) { null }
- }
- }
- } catch (e: Exception) {
- null
- }
- }
- }
-
- fun getImage(index: Int): FileX? =
- metadata.imageList?.getOrNull(index)?.let { findFile(it) }
-
- @Suppress("BlockingMethodInNonBlockingContext")
- suspend fun putImage(index: Int, fileName: String, data: ByteArray) = coroutineScope {
- val file = cacheFolder.getChild(fileName)
-
- if (!file.exists())
- file.createNewFile()
-
- file.writeBytes(data)
- setMetadata { metadata -> metadata.imageList!![index] = fileName }
- }
-
- private val lock = ConcurrentHashMap()
- @Suppress("BlockingMethodInNonBlockingContext")
- fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
- val downloadFolder = downloadFolder ?: return@launch
-
- if (lock[galleryID]?.isLocked == true)
- return@launch
-
- (lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
- val cacheMetadata = cacheFolder.getChild(".metadata")
- val downloadMetadata = downloadFolder.getChild(".metadata")
-
- if (!cacheMetadata.exists())
- return@launch
-
- if (cacheMetadata.exists()) {
- kotlin.runCatching {
- if (!downloadMetadata.exists())
- downloadMetadata.createNewFile()
-
- downloadMetadata.writeText(Json.encodeToString(metadata))
- }
- }
-
- val cacheThumbnail = cacheFolder.getChild(".thumbnail")
- val downloadThumbnail = downloadFolder.getChild(".thumbnail")
-
- if (cacheThumbnail.exists()) {
- kotlin.runCatching {
- if (!downloadThumbnail.exists())
- downloadThumbnail.createNewFile()
-
- downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
- source.copyTo(target)
- } }
- cacheThumbnail.delete()
- }
- }
-
- metadata.imageList?.forEach { imageName ->
- imageName ?: return@forEach
- val target = downloadFolder.getChild(imageName)
- val source = cacheFolder.getChild(imageName)
-
- if (!source.exists())
- return@forEach
-
- kotlin.runCatching {
- if (!target.exists())
- target.createNewFile()
-
- target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
- source.copyTo(target)
- } }
- }
- }
-
- cacheFolder.deleteRecursively()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
deleted file mode 100644
index 77349b4b..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt
+++ /dev/null
@@ -1,123 +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 .
- */
-
-package xyz.quaver.pupil.util.downloader
-
-import android.content.Context
-import android.content.ContextWrapper
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-import okhttp3.Call
-import xyz.quaver.io.FileX
-import xyz.quaver.io.util.*
-import xyz.quaver.pupil.client
-import xyz.quaver.pupil.services.DownloadService
-import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.formatDownloadFolder
-
-class DownloadManager private constructor(context: Context) : ContextWrapper(context) {
-
- companion object {
- @Volatile private var instance: DownloadManager? = null
-
- fun getInstance(context: Context) =
- instance ?: synchronized(this) {
- instance ?: DownloadManager(context).also { instance = it }
- }
- }
-
- val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
-
- val downloadFolder: FileX
- get() = kotlin.runCatching {
- FileX(this, Preferences.get("download_folder"))
- }.getOrElse {
- Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
- defaultDownloadFolder
- }
-
- private var prevDownloadFolder: FileX? = null
- private var downloadFolderMapInstance: MutableMap? = null
- val downloadFolderMap: MutableMap
- @Synchronized
- get() {
- if (prevDownloadFolder != downloadFolder) {
- prevDownloadFolder = downloadFolder
- downloadFolderMapInstance = run {
- val file = downloadFolder.getChild(".download")
- val data = if (file.exists())
- kotlin.runCatching {
- file.readText()?.let{ Json.decodeFromString>(it) }
- }.onFailure { file.delete() }.getOrNull()
- else
- null
- data ?: run {
- file.createNewFile()
- mutableMapOf()
- }
- }
- }
-
- return downloadFolderMapInstance ?: mutableMapOf()
- }
-
-
- @Synchronized
- fun isDownloading(galleryID: Int): Boolean {
- val isThisGallery: (Call) -> Boolean = { !it.isCanceled() && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
-
- return downloadFolderMap.containsKey(galleryID)
- && client.dispatcher.let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
- }
-
- @Synchronized
- fun getDownloadFolder(galleryID: Int): FileX? =
- downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
-
- fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
- val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
- ?.formatDownloadFolder() ?: return@launch
-
- val folder = downloadFolder.getChild(name)
-
- downloadFolderMap[galleryID] = name
-
- downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
- downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
-
- if (folder.exists()) return@launch
- folder.mkdir()
- }
-
- @Synchronized
- fun deleteDownloadFolder(galleryID: Int) {
- downloadFolderMap[galleryID]?.let {
- kotlin.runCatching {
- downloadFolder.getChild(it).deleteRecursively()
- downloadFolderMap.remove(galleryID)
-
- downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
- downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml
deleted file mode 100644
index 71d51f57..00000000
--- a/app/src/main/res/layout/reader_activity.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/reader_eye_card.xml b/app/src/main/res/layout/reader_eye_card.xml
deleted file mode 100644
index 11343d53..00000000
--- a/app/src/main/res/layout/reader_eye_card.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/reader_item.xml b/app/src/main/res/layout/reader_item.xml
deleted file mode 100644
index 0cee0987..00000000
--- a/app/src/main/res/layout/reader_item.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 1ef173db..ab8f1ce3 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -14,6 +14,11 @@
検索設定
設定
アップデートダウンロード中
+ 同人誌
+ 漫画
+ アーティストCG
+ ゲームCG
+ イメージまとめ
新しいアップデートがあります
注意
その他
@@ -92,7 +97,7 @@
おすすめ
イメージを隠す
削除
- ダウンロード
+ ダウンロード
ブックマークバックアップ
ブックマーク復元
バックアップファイルを作成しました
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index c97a0323..2b01e60b 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -13,6 +13,11 @@
검색 설정
설정
업데이트 다운로드중…
+ 동인지
+ 만화
+ 아티스트 CG
+ 게임 CG
+ 이미지 모음
업데이트가 있습니다!
경고
결과 없음\n해결법
@@ -91,7 +96,7 @@
미리보기
이미지 숨기기
삭제
- 다운로드
+ 다운로드
즐겨찾기 백업
즐겨찾기 복원
백업 파일을 생성하였습니다
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fb70ccb8..44f6410a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -83,9 +83,15 @@
Move to page %1$d
- DOWNLOAD
+ Download
DELETE
+ Doujinshi
+ Manga
+ Artist CG
+ Game CG
+ Image Set
+
Update available
Download Completed
Click here to update
diff --git a/build.gradle b/build.gradle
index 2eb3321f..5010f45b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,7 @@ buildscript {
plugins {
id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false
+ id 'com.google.dagger.hilt.android' version '2.44' apply false
}
allprojects {