From 72b0fa78bb4b2d5fa3d08606c1c22c526cb31708 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:19:29 -0800 Subject: [PATCH] Migrated networking to ktor --- app/build.gradle | 93 +- app/src/main/java/xyz/quaver/pupil/Pupil.kt | 2 - .../pupil/adapters/GalleryBlockAdapter.kt | 323 ------- .../quaver/pupil/adapters/ThumbnailAdapter.kt | 52 -- .../pupil/adapters/ThumbnailPageAdapter.kt | 52 -- .../java/xyz/quaver/pupil/hitomi/common.kt | 8 +- .../java/xyz/quaver/pupil/hitomi/search.kt | 20 +- .../pupil/networking/HitomiHttpClient.kt | 208 +++++ .../java/xyz/quaver/pupil/networking/Node.kt | 107 +++ .../quaver/pupil/networking/SearchQuery.kt | 48 + .../quaver/pupil/services/DownloadService.kt | 20 +- .../xyz/quaver/pupil/types/Suggestions.kt | 50 - .../java/xyz/quaver/pupil/ui/MainActivity.kt | 852 +----------------- .../quaver/pupil/ui/composable/ContentType.kt | 5 + .../pupil/ui/composable/DevicePosture.kt | 34 + .../xyz/quaver/pupil/ui/composable/MainApp.kt | 92 ++ .../ui/composable/MainNavigationActions.kt | 38 + .../composable/NavigationContentPosition.kt | 5 + .../ui/composable/NavigationDrawerContent.kt | 59 ++ .../pupil/ui/composable/NavigationType.kt | 5 + .../quaver/pupil/ui/dialog/GalleryDialog.kt | 255 ------ .../ui/fragment/ManageStorageFragment.kt | 263 ------ .../java/xyz/quaver/pupil/ui/theme/Color.kt | 67 ++ .../java/xyz/quaver/pupil/ui/theme/Theme.kt | 90 ++ .../pupil/ui/view/FloatingSearchView.kt | 216 ----- .../xyz/quaver/pupil/ui/view/MainView.java | 462 ---------- .../xyz/quaver/pupil/ui/view/ProgressCard.kt | 72 -- .../java/xyz/quaver/pupil/ui/view/TagChip.kt | 100 -- .../xyz/quaver/pupil/ui/view/TagChipGroup.kt | 100 -- .../pupil/ui/viewmodel/MainViewModel.kt | 14 + .../xyz/quaver/pupil/util/downloader/Cache.kt | 2 +- .../pupil/util/downloader/DownloadManager.kt | 4 +- .../main/java/xyz/quaver/pupil/util/proxy.kt | 2 +- .../java/xyz/quaver/pupil/util/translation.kt | 10 +- .../main/java/xyz/quaver/pupil/util/update.kt | 2 +- app/src/main/res/drawable-hdpi/ic_email.png | Bin 325 -> 0 bytes app/src/main/res/drawable-hdpi/ic_help.png | Bin 571 -> 0 bytes app/src/main/res/drawable-hdpi/ic_home.png | Bin 279 -> 0 bytes .../main/res/drawable-hdpi/ic_settings.png | Bin 469 -> 0 bytes app/src/main/res/drawable-mdpi/ic_email.png | Bin 235 -> 0 bytes app/src/main/res/drawable-mdpi/ic_help.png | Bin 345 -> 0 bytes app/src/main/res/drawable-mdpi/ic_home.png | Bin 222 -> 0 bytes .../main/res/drawable-mdpi/ic_settings.png | Bin 352 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_email.png | Bin 383 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_help.png | Bin 676 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_home.png | Bin 314 -> 0 bytes .../main/res/drawable-xhdpi/ic_settings.png | Bin 628 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_email.png | Bin 550 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_help.png | Bin 1000 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_home.png | Bin 395 -> 0 bytes .../main/res/drawable-xxhdpi/ic_settings.png | Bin 914 -> 0 bytes app/src/main/res/drawable/app_icon.xml | 12 + app/src/main/res/drawable/arrow_right.xml | 4 - app/src/main/res/drawable/cancel.xml | 4 - app/src/main/res/drawable/clock_end.xml | 4 - app/src/main/res/drawable/clock_start.xml | 4 - app/src/main/res/drawable/github_circle.xml | 4 - app/src/main/res/drawable/ic_jump.xml | 4 - app/src/main/res/drawable/ic_message.xml | 4 - app/src/main/res/drawable/ic_progressbar.xml | 5 - .../res/drawable/ic_progressbar_cache.xml | 5 - .../res/drawable/ic_progressbar_complete.xml | 13 - .../ic_progressbar_complete_cache.xml | 13 - app/src/main/res/drawable/icon.xml | 30 - app/src/main/res/drawable/icon_red.xml | 30 - app/src/main/res/drawable/menu.xml | 4 - app/src/main/res/drawable/shuffle_variant.xml | 4 - app/src/main/res/drawable/side_nav_bar.png | Bin 68495 -> 0 bytes app/src/main/res/drawable/sort_variant.xml | 8 - app/src/main/res/drawable/thumb_drawable.xml | 6 - app/src/main/res/drawable/track.xml | 6 - app/src/main/res/drawable/track_drawable.xml | 6 - app/src/main/res/layout/gallery_dialog.xml | 132 --- .../res/layout/gallery_dialog_details.xml | 40 - .../layout/gallery_dialog_dotindicator.xml | 34 - .../main/res/layout/gallery_dialog_tags.xml | 40 - app/src/main/res/layout/galleryblock_item.xml | 160 ---- app/src/main/res/layout/main_activity.xml | 42 - .../main/res/layout/main_activity_content.xml | 127 --- app/src/main/res/layout/mirrors_item.xml | 50 - app/src/main/res/layout/nav_header_main.xml | 32 - .../main/res/layout/swipe_pageturn_view.xml | 30 - .../main/res/menu/activity_main_drawer.xml | 61 -- app/src/main/res/menu/main.xml | 49 - app/src/main/res/values-ja/strings.xml | 8 +- app/src/main/res/values-ko/strings.xml | 8 +- app/src/main/res/values/dimen.xml | 10 +- app/src/main/res/values/ids.xml | 3 - app/src/main/res/values/strings.xml | 8 +- .../java/xyz/quaver/pupil/ExampleUnitTest.kt | 14 +- build.gradle | 2 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 93 files changed, 915 insertions(+), 3841 deletions(-) delete mode 100644 app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/networking/Node.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt delete mode 100644 app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt delete mode 100644 app/src/main/res/drawable-hdpi/ic_email.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_home.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_settings.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_email.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_home.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_settings.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_email.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_home.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_settings.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_email.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_home.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings.png create mode 100644 app/src/main/res/drawable/app_icon.xml delete mode 100644 app/src/main/res/drawable/arrow_right.xml delete mode 100644 app/src/main/res/drawable/cancel.xml delete mode 100644 app/src/main/res/drawable/clock_end.xml delete mode 100644 app/src/main/res/drawable/clock_start.xml delete mode 100644 app/src/main/res/drawable/github_circle.xml delete mode 100644 app/src/main/res/drawable/ic_jump.xml delete mode 100644 app/src/main/res/drawable/ic_message.xml delete mode 100644 app/src/main/res/drawable/ic_progressbar.xml delete mode 100644 app/src/main/res/drawable/ic_progressbar_cache.xml delete mode 100644 app/src/main/res/drawable/ic_progressbar_complete.xml delete mode 100644 app/src/main/res/drawable/ic_progressbar_complete_cache.xml delete mode 100644 app/src/main/res/drawable/icon.xml delete mode 100644 app/src/main/res/drawable/icon_red.xml delete mode 100644 app/src/main/res/drawable/menu.xml delete mode 100644 app/src/main/res/drawable/shuffle_variant.xml delete mode 100644 app/src/main/res/drawable/side_nav_bar.png delete mode 100644 app/src/main/res/drawable/sort_variant.xml delete mode 100644 app/src/main/res/drawable/thumb_drawable.xml delete mode 100644 app/src/main/res/drawable/track.xml delete mode 100644 app/src/main/res/drawable/track_drawable.xml delete mode 100644 app/src/main/res/layout/gallery_dialog.xml delete mode 100644 app/src/main/res/layout/gallery_dialog_details.xml delete mode 100644 app/src/main/res/layout/gallery_dialog_dotindicator.xml delete mode 100644 app/src/main/res/layout/gallery_dialog_tags.xml delete mode 100644 app/src/main/res/layout/galleryblock_item.xml delete mode 100644 app/src/main/res/layout/main_activity.xml delete mode 100644 app/src/main/res/layout/main_activity_content.xml delete mode 100644 app/src/main/res/layout/mirrors_item.xml delete mode 100644 app/src/main/res/layout/nav_header_main.xml delete mode 100644 app/src/main/res/layout/swipe_pageturn_view.xml delete mode 100644 app/src/main/res/menu/activity_main_drawer.xml delete mode 100644 app/src/main/res/menu/main.xml diff --git a/app/build.gradle b/app/build.gradle index d123fc75..b0cf5058 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,27 +14,11 @@ if (file("google-services.json").exists()) { logger.lifecycle("Firebase Disabled") } -ext { - okhttp_version = "3.12.12" -} - -configurations { - all { - resolutionStrategy { - eachDependency { DependencyResolveDetails details -> - if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") { - // OkHttp drops support before 5.0 since 3.13.0 - details.useVersion okhttp_version - } - } - } - } -} - android { + namespace 'xyz.quaver.pupil' defaultConfig { applicationId "xyz.quaver.pupil" - minSdkVersion 16 + minSdkVersion 21 compileSdk 34 targetSdkVersion 34 versionCode 69 @@ -44,8 +28,6 @@ android { } buildTypes { debug { - defaultConfig.minSdkVersion 21 - minifyEnabled false shrinkResources false @@ -65,33 +47,58 @@ android { } buildFeatures { viewBinding true - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" + compose true + buildConfig true } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.9" } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" - implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" + implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0" - implementation "androidx.appcompat:appcompat:1.4.1" - implementation "androidx.activity:activity-ktx:1.4.0" - implementation "androidx.fragment:fragment-ktx:1.4.1" - implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.fragment:fragment-ktx:1.6.2" + implementation "androidx.preference:preference-ktx:1.2.1" + implementation "androidx.recyclerview:recyclerview:1.3.2" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.biometric:biometric:1.1.0" - implementation "androidx.work:work-runtime-ktx:2.7.1" + implementation "androidx.work:work-runtime-ktx:2.9.0" + + implementation platform("androidx.compose:compose-bom:2024.02.00") + + implementation "androidx.compose.material3:material3" + implementation "androidx.compose.material3:material3-window-size-class" + implementation 'androidx.compose.foundation:foundation' + 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' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation 'androidx.compose.material:material-icons-extended' + implementation 'androidx.activity:activity-compose:1.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' + implementation "com.google.accompanist:accompanist-adaptive:0.34.0" + implementation "androidx.navigation:navigation-compose:2.7.7" + + implementation "androidx.paging:paging-compose:3.2.1" + + implementation "io.ktor:ktor-client-core:2.3.8" + implementation "io.ktor:ktor-client-okhttp:2.3.8" implementation "com.daimajia.swipelayout:library:1.2.0@aar" @@ -112,10 +119,10 @@ dependencies { implementation 'com.github.piasy:BigImageViewer:1.8.1' implementation 'com.github.piasy:FrescoImageLoader:1.8.1' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1' - implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0' + implementation 'com.facebook.fresco:imagepipeline-okhttp3:3.1.3' //noinspection GradleDependency - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2" @@ -133,9 +140,9 @@ dependencies { implementation "xyz.quaver:floatingsearchview:1.1.7" testImplementation "junit:junit:4.13.2" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1" - androidTestImplementation "androidx.test.ext:junit:1.1.3" - androidTestImplementation "androidx.test:rules:1.4.0" - androidTestImplementation "androidx.test:runner:1.4.0" - androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" + androidTestImplementation "androidx.test.ext:junit:1.1.5" + androidTestImplementation "androidx.test:rules:1.5.0" + androidTestImplementation "androidx.test:runner:1.5.2" + androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" } \ 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 b21d4760..e2755159 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -44,8 +44,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import xyz.quaver.io.FileX -import xyz.quaver.pupil.hitomi.evaluationContext -import xyz.quaver.pupil.hitomi.readText import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.util.* import java.io.File diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt deleted file mode 100644 index 91cec1c5..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ /dev/null @@ -1,323 +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.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import androidx.vectordrawable.graphics.drawable.Animatable2Compat -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import com.daimajia.swipe.SwipeLayout -import com.daimajia.swipe.adapters.RecyclerSwipeAdapter -import com.daimajia.swipe.interfaces.SwipeAdapterInterface -import com.github.piasy.biv.loader.ImageLoader -import kotlinx.coroutines.* -import xyz.quaver.io.util.getChild -import xyz.quaver.pupil.R -import xyz.quaver.pupil.databinding.GalleryblockItemBinding -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.favorites -import xyz.quaver.pupil.hitomi.getGallery -import xyz.quaver.pupil.hitomi.getGalleryInfo -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.ui.view.ProgressCard -import xyz.quaver.pupil.util.Preferences -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.wordCapitalize -import java.io.File - -class GalleryBlockAdapter(private val galleries: List) : RecyclerSwipeAdapter(), SwipeAdapterInterface { - - var updateAll = true - var thin: Boolean = Preferences["thin"] - - inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) { - private var galleryID: Int = 0 - - init { - CoroutineScope(Dispatchers.Main).launch { - while (updateAll) { - updateProgress(itemView.context) - delay(1000) - } - } - } - - private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch { - with(binding.galleryblockCard) { - val imageList = Cache.getInstance(context, galleryID).metadata.imageList - - if (imageList == null) { - max = 0 - return@with - } - - progress = imageList.count { it != null } - max = imageList.size - - this@GalleryViewHolder.binding.galleryblockId.setOnClickListener { - (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( - ClipData.newPlainText("gallery_id", galleryID.toString()) - ) - Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - - type = if (!imageList.contains(null)) { - val downloadManager = DownloadManager.getInstance(context) - - if (downloadManager.getDownloadFolder(galleryID) == null) - ProgressCard.Type.CACHE - else - ProgressCard.Type.DOWNLOAD - } else - ProgressCard.Type.LOADING - } - } - - fun bind(galleryID: Int) { - this.galleryID = galleryID - updateProgress(itemView.context) - - val cache = Cache.getInstance(itemView.context, galleryID) - - CoroutineScope(Dispatchers.IO).launch { - val galleryBlock = cache.getGalleryBlock() ?: return@launch - - launch(Dispatchers.Main) { - val resources = itemView.context.resources - val languages = resources.getStringArray(R.array.languages).map { - it.split("|").let { split -> - Pair(split[0], split[1]) - } - }.toMap() - - val artists = galleryBlock.artists - val series = galleryBlock.series - - binding.galleryblockThumbnail.apply { - setOnClickListener { - itemView.performClick() - } - setOnLongClickListener { - itemView.performLongClick() - } - setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant)) - setImageLoaderCallback(object: ImageLoader.Callback { - override fun onFail(error: Exception?) { - Cache.delete(context, galleryID) - } - - override fun onCacheHit(imageType: Int, image: File?) {} - override fun onCacheMiss(imageType: Int, image: File?) {} - override fun onFinish() {} - override fun onProgress(progress: Int) {} - override fun onStart() {} - override fun onSuccess(image: File?) {} - }) - ssiv?.recycle() - CoroutineScope(Dispatchers.IO).launch { - cache.getThumbnail().let { launch(Dispatchers.Main) { - showImage(it) - } } - } - } - - binding.galleryblockTitle.text = galleryBlock.title - with(binding.galleryblockArtist) { - text = artists.joinToString { it.wordCapitalize() } - visibility = when { - artists.isNotEmpty() -> View.VISIBLE - else -> View.GONE - } - - CoroutineScope(Dispatchers.IO).launch { - val gallery = runCatching { - getGallery(galleryID) - }.getOrNull() - - if (gallery?.groups?.isNotEmpty() != true) - return@launch - - launch(Dispatchers.Main) { - text = context.getString( - R.string.galleryblock_artist_with_group, - artists.joinToString { it.wordCapitalize() }, - gallery.groups.joinToString { it.wordCapitalize() } - ) - } - } - } - with(binding.galleryblockSeries) { - text = - resources.getString( - R.string.galleryblock_series, - series.joinToString(", ") { it.wordCapitalize() }) - visibility = when { - series.isNotEmpty() -> View.VISIBLE - else -> View.GONE - } - } - binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize() - with(binding.galleryblockLanguage) { - text = - resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) - visibility = when { - !galleryBlock.language.isNullOrEmpty() -> View.VISIBLE - else -> View.GONE - } - } - - with(binding.galleryblockTagGroup) { - onClickListener = { - onChipClickedHandler.forEach { callback -> - callback.invoke(it) - } - } - - tags.clear() - - CoroutineScope(Dispatchers.IO).launch { - tags.addAll( - galleryBlock.relatedTags.sortedBy { - val tag = Tag.parse(it) - - if (favoriteTags.contains(tag)) - -1 - else - when(Tag.parse(it).area) { - "female" -> 0 - "male" -> 1 - else -> 2 - } - }.map { - Tag.parse(it) - } - ) - - launch(Dispatchers.Main) { - refresh() - } - } - } - - binding.galleryblockId.text = galleryBlock.id.toString() - binding.galleryblockPagecount.text = "-" - CoroutineScope(Dispatchers.IO).launch { - val pageCount = kotlin.runCatching { - getGalleryInfo(galleryBlock.id).files.size - }.getOrNull() ?: return@launch - withContext(Dispatchers.Main) { - binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount) - } - } - - with(binding.galleryblockFavorite) { - setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty) - setOnClickListener { - when { - favorites.contains(galleryBlock.id) -> { - favorites.remove(galleryBlock.id) - - setImageResource(R.drawable.ic_star_empty) - } - else -> { - favorites.add(galleryBlock.id) - - setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply { - this ?: return@apply - - registerAnimationCallback(object: Animatable2Compat.AnimationCallback() { - override fun onAnimationEnd(drawable: Drawable?) { - setImageResource(R.drawable.ic_star_filled) - } - }) - start() - }) - } - } - } - } - - - } - } - - // Make some views invisible to make it thinner - if (thin) { - binding.galleryblockTagGroup.visibility = View.GONE - } - } - } - - val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() - var onDownloadClickedHandler: ((Int) -> Unit)? = null - var onDeleteClickedHandler: ((Int) -> Unit)? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is GalleryViewHolder) { - val galleryID = galleries[position] - - holder.bind(galleryID) - - holder.binding.galleryblockCard.binding.download.setOnClickListener { - onDownloadClickedHandler?.invoke(position) - } - - holder.binding.galleryblockCard.binding.delete.setOnClickListener { - onDeleteClickedHandler?.invoke(position) - } - - mItemManger.bindView(holder.binding.root, position) - - holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener { - override fun onStartOpen(layout: SwipeLayout?) { - mItemManger.closeAllExcept(layout) - - holder.binding.galleryblockCard.binding.download.text = - if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID)) - holder.binding.root.context.getString(android.R.string.cancel) - else - holder.binding.root.context.getString(R.string.main_download) - } - - override fun onClose(layout: SwipeLayout?) {} - override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {} - override fun onOpen(layout: SwipeLayout?) {} - override fun onStartClose(layout: SwipeLayout?) {} - override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {} - }) - } - } - - override fun getItemCount() = galleries.size - - override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt deleted file mode 100644 index 3b9a081b..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt +++ /dev/null @@ -1,52 +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.net.Uri -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import com.github.piasy.biv.view.BigImageView -import xyz.quaver.pupil.R - -class ThumbnailAdapter(var thumbnails: List) : RecyclerView.Adapter() { - - class ViewHolder(val view: BigImageView) : RecyclerView.ViewHolder(view) { - fun clear() { - view.ssiv?.recycle() - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(BigImageView(parent.context).apply { - setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant)) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.view.showImage(Uri.parse(thumbnails[position])) - } - - override fun getItemCount() = thumbnails.size - - override fun onViewRecycled(holder: ViewHolder) { - holder.clear() - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt deleted file mode 100644 index 22667059..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt +++ /dev/null @@ -1,52 +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.adapters - -import android.view.ViewGroup -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlin.math.min - -class ThumbnailPageAdapter(private val thumbnails: List) : RecyclerView.Adapter() { - - class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(RecyclerView(parent.context).apply { - val layoutManager = GridLayoutManager(parent.context, 3) - val adapter = ThumbnailAdapter(listOf()) - - this.layoutManager = layoutManager - this.adapter = adapter - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - (holder.view.adapter as ThumbnailAdapter).apply { - thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size)) - notifyDataSetChanged() - - (holder.view.layoutManager as GridLayoutManager).scrollToPosition(8) - } - } - - override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0 - -} \ 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 index abea6418..13d92e0e 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt @@ -110,7 +110,7 @@ fun URL.readText(settings: HeaderSetter? = null): String { 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() + 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 { @@ -119,7 +119,7 @@ fun URL.readBytes(settings: HeaderSetter? = null): ByteArray { 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() + 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") @@ -161,8 +161,8 @@ object gg { } override fun onResponse(call: Call, response: Response) { - if (!call.isCanceled) { - response.body()?.use { + if (!call.isCanceled()) { + response.body?.use { continuation.resume(it.string()) { call.cancel() } diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt index b4eaecf6..ef622bce 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt @@ -50,7 +50,7 @@ fun sanitize(input: String) : String { } fun getIndexVersion(name: String) = - URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() + URL("$protocol//${xyz.quaver.pupil.networking.domain}/$name/version?_=${System.currentTimeMillis()}").readText() //search.js fun getGalleryIDsForQuery(query: String) : Set { @@ -115,7 +115,7 @@ fun getSuggestionsForQuery(query: String) : List { 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//$domain/$index_dir/$field.$tag_index_version.data" + 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") @@ -162,8 +162,8 @@ fun getSuggestionsFromData(field: String, data: Pair) : List { val nozomiAddress = when(area) { - null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" - else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" + 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 { @@ -185,7 +185,7 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set< } fun getGalleryIDsFromData(data: Pair) : Set { - val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" + 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") @@ -216,10 +216,10 @@ fun getGalleryIDsFromData(data: Pair) : Set { fun getNodeAtAddress(field: String, address: Long) : Node? { val url = when(field) { - "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index" - "languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index" - "nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index" - else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" + "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)) @@ -233,7 +233,7 @@ fun getURLAtRange(url: String, range: LongRange) : ByteArray { .header("Range", "bytes=${range.first}-${range.last}") .build() - return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf() + return client.newCall(request).execute().body?.use { it.bytes() } ?: byteArrayOf() } @OptIn(ExperimentalUnsignedTypes::class) diff --git a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt new file mode 100644 index 00000000..6ec8cd21 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt @@ -0,0 +1,208 @@ +package xyz.quaver.pupil.networking + +import androidx.collection.mutableIntSetOf +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.okhttp.OkHttp +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import xyz.quaver.pupil.hitomi.max_node_size +import java.nio.ByteBuffer +import java.nio.IntBuffer + +const val domain = "ltn.hitomi.la" +const val galleryBlockExtension = ".html" +const val galleryBlockDir = "galleryblock" +const val nozomiExtension = ".nozomi" + +const val compressedNozomiPrefix = "n" + +const val B = 16 +const val indexDir = "tagindex" +const val galleriesIndexDir = "galleriesindex" +const val languagesIndexDir = "languagesindex" +const val nozomiURLIndexDir = "nozomiurlindex" + +fun IntBuffer.toSet(): Set { + val result = mutableSetOf() + + while (this.hasRemaining()) { + result.add(this.get()) + } + + return result +} + +class HitomiHttpClient { + private val httpClient = HttpClient(OkHttp) + + private var _tagIndexVersion: String? = null + private suspend fun getTagIndexVersion(): String = + _tagIndexVersion ?: getIndexVersion("tagindex").also { + _tagIndexVersion = it + } + + private var _galleriesIndexVersion: String? = null + private suspend fun getGalleriesIndexVersion(): String = + _galleriesIndexVersion ?: getIndexVersion("galleriesindex").also { + _galleriesIndexVersion = it + } + + private suspend fun getIndexVersion(name: String): String = withContext(Dispatchers.IO) { + httpClient.get("https://$domain/$name/version?_=${System.currentTimeMillis()}").bodyAsText() + } + + private suspend fun getURLAtRange(url: String, range: LongRange): ByteBuffer { + val response: HttpResponse = withContext(Dispatchers.IO) { + httpClient.get(url) { + header("Range", "bytes=${range.first}-${range.last}") + } + } + + val result: ByteArray = response.body() + + return ByteBuffer.wrap(result) + } + + private suspend fun getNodeAtAddress(field: String, address: Long): Node { + val url = when (field) { + "galleries" -> "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.index" + "languages" -> "https://$domain/$galleriesIndexDir/languages.${getGalleriesIndexVersion()}.index" + "nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${getGalleriesIndexVersion()}.index" + else -> "https://$domain/$indexDir/$field.${getTagIndexVersion()}.index" + } + + return Node.decodeNode( + getURLAtRange(url, address until (address+max_node_size)) + ) + } + + private suspend fun bSearch( + field: String, + key: Node.Key, + node: Node + ): Node.Data? { + if (node.keys.isEmpty()) { + return null + } + + val (matched, index) = node.locateKey(key) + + if (matched) { + return node.datas[index] + } else if (node.isLeaf) { + return null + } + + val nextNode = getNodeAtAddress(field, node.subNodeAddresses[index]) + return bSearch(field, key, nextNode) + } + + private suspend fun getGalleryIDsFromData(offset: Long, length: Int): IntBuffer { + val url = "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.data" + if (length > 100000000 || length <= 0) { + error("length $length is too long") + } + + return getURLAtRange(url, offset until (offset+length)).asIntBuffer() + } + + suspend fun getGalleryIDsFromNozomi( + area: String?, + tag: String, + language: String + ): IntBuffer { + val nozomiAddress = if (area == null) { + "https://$domain/$compressedNozomiPrefix/$tag-$language$nozomiExtension" + } else { + "https://$domain/$compressedNozomiPrefix/$area/$tag-$language$nozomiExtension" + } + + val response: HttpResponse = withContext(Dispatchers.IO) { + httpClient.get(nozomiAddress) + } + + val result: ByteArray = response.body() + + return ByteBuffer.wrap(result).asIntBuffer() + } + + suspend fun getGalleryIDsForQuery(query: SearchQuery.Tag, language: String = "all"): IntBuffer = when (query.namespace) { + "female", "male" -> getGalleryIDsFromNozomi("tag", query.toString(), language) + "language" -> getGalleryIDsFromNozomi(null, "index", query.tag) + null -> { + val key = Node.Key(query.tag) + + val node = getNodeAtAddress("galleries", 0) + val data = bSearch("galleries", key, node) + + if (data != null) getGalleryIDsFromData(data.offset, data.length) else IntBuffer.allocate(0) + } + else -> getGalleryIDsFromNozomi(query.namespace, query.tag, language) + } + + suspend fun search(query: SearchQuery?): Set = when (query) { + is SearchQuery.Tag -> getGalleryIDsForQuery(query).toSet() + is SearchQuery.Not -> coroutineScope { + val allGalleries = async { + getGalleryIDsFromNozomi(null, "index", "all") + } + + val queriedGalleries = search(query.query) + + val result = mutableSetOf() + + with (allGalleries.await()) { + while (this.hasRemaining()) { + val gallery = this.get() + + if (gallery in queriedGalleries) { + result.add(gallery) + } + } + } + + result + } + is SearchQuery.And -> coroutineScope { + val queries = query.queries.map { query -> + async { + search(query) + } + } + + val result = queries.first().await().toMutableSet() + + queries.drop(1).forEach { + val queryResult = it.await() + + result.retainAll(queryResult) + } + + result + } + is SearchQuery.Or -> coroutineScope { + val queries = query.queries.map { query -> + async { + search(query) + } + } + + val result = mutableSetOf() + + queries.forEach { + val queryResult = it.await() + result.addAll(queryResult) + } + + result + } + null -> getGalleryIDsFromNozomi(null, "index", "all").toSet() + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/networking/Node.kt b/app/src/main/java/xyz/quaver/pupil/networking/Node.kt new file mode 100644 index 00000000..6e90284b --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/Node.kt @@ -0,0 +1,107 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package xyz.quaver.pupil.networking + +import java.nio.ByteBuffer +import java.security.MessageDigest +import kotlin.math.min + +private fun sha256(data: ByteArray): ByteArray = + MessageDigest.getInstance("SHA-256").digest(data) + +private fun hashTerm(term: String): UByteArray = + sha256(term.toByteArray()).sliceArray(0..<4).toUByteArray() + +data class Node( + val keys: List, + val datas: List, + val subNodeAddresses: List +) { + data class Key( + private val key: UByteArray + ): Comparable { + + constructor(term: String): this(hashTerm(term)) + + override fun compareTo(other: Key): Int { + val minSize = min(this.key.size, other.key.size) + + for (i in 0.. other.key[i]) { + return 1 + } + } + + return 0 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Key + + return key.contentEquals(other.key) + } + + override fun hashCode(): Int { + return key.contentHashCode() + } + } + + data class Data( + val offset: Long, + val length: Int + ) + + companion object { + fun decodeNode(buffer: ByteBuffer): Node { + val numberOfKeys = buffer.int + val keys = mutableListOf() + + for (i in 0..() + + for (i in 0..() + + for (i in 0.. { + val index = keys.indexOfFirst { key -> key <= target } + + if (index == -1) { + return Pair(false, keys.size) + } + + return Pair(keys[index] == target, index) + } +} + diff --git a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt new file mode 100644 index 00000000..f242d6e1 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt @@ -0,0 +1,48 @@ +package xyz.quaver.pupil.networking + +sealed interface SearchQuery { + data class Tag( + val namespace: String?, + val tag: String + ): SearchQuery { + companion object { + fun parseTag(tag: String): Tag { + val splitTag = tag.split(':', limit = 1) + + return if (splitTag.size == 1) { + Tag(null, tag) + } else { + Tag(splitTag[0], splitTag[1]) + } + } + } + + override fun toString() = if (namespace == null) tag else "$namespace:$tag" + } + + + data class And( + val queries: List + ): SearchQuery { + init { + if (queries.isEmpty()) { + error("queries cannot be empty") + } + } + } + + data class Or( + val queries: List + ): SearchQuery { + init { + if (queries.isEmpty()) { + error("queries cannot be empty") + } + } + } + + data class Not( + val query: SearchQuery + ): SearchQuery + +} diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt index 54bf595e..77f27cc6 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt @@ -149,7 +149,7 @@ class DownloadService : Service() { override fun source(): BufferedSource { if (bufferedSource == null) - bufferedSource = Okio.buffer(source(responseBody.source())) + bufferedSource = source(responseBody.source()).buffer() return bufferedSource!! } @@ -177,7 +177,7 @@ class DownloadService : Service() { var limit = 10 while (response?.isSuccessful != true) { - if (response?.code() == 503) { + if (response?.code == 503) { Thread.sleep(200) } else if (--limit < 0) break @@ -191,7 +191,7 @@ class DownloadService : Service() { response = chain.proceed(request) response!!.newBuilder() - .body(response.body()?.let { + .body(response.body?.let { ProgressResponseBody(request.tag(), it, progressListener) }).build() } @@ -228,11 +228,11 @@ class DownloadService : Service() { 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() + 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 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) @@ -257,13 +257,13 @@ class DownloadService : Service() { } fun cancel(startId: Int? = null) { - client.dispatcher().queuedCalls().filter { + 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 { + client.dispatcher.runningCalls().filter { it.request().tag() is Tag }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } @@ -278,13 +278,13 @@ class DownloadService : Service() { } fun cancel(galleryID: Int, startId: Int? = null) { - client.dispatcher().queuedCalls().filter { + 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 { + client.dispatcher.runningCalls().filter { (it.request().tag() as? Tag)?.galleryID == galleryID }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } @@ -350,7 +350,7 @@ class DownloadService : Service() { val queued = mutableSetOf() if (priority) { - client.dispatcher().queuedCalls().forEach { + client.dispatcher.queuedCalls().forEach { val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach if (queued.add(queuedID)) diff --git a/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt b/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt deleted file mode 100644 index 86cef085..00000000 --- a/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt +++ /dev/null @@ -1,50 +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.types - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.pupil.hitomi.Suggestion -import xyz.quaver.pupil.util.translations - -@Parcelize -data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { - constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) - - @IgnoredOnParcel - override val body = - if (translations[s] != null) - "${translations[s]} ($s)" - else - s -} - -@Parcelize -class Suggestion(override val body: String) : SearchSuggestion - -@Parcelize -class NoResultSuggestion(override val body: String) : SearchSuggestion - -@Parcelize -class LoadingSuggestion(override val body: String) : SearchSuggestion - -@Parcelize -@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY") -class FavoriteHistorySwitch(override val body: String) : SearchSuggestion \ 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 e862fcc0..ff619f34 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -18,842 +18,36 @@ package xyz.quaver.pupil.ui -import android.Manifest -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build import android.os.Bundle -import android.text.InputType -import android.text.util.Linkify -import android.view.KeyEvent -import android.view.MenuItem -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.EditText -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.view.GravityCompat -import androidx.core.view.ViewCompat -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.navigation.NavigationView -import com.google.android.material.snackbar.Snackbar -import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.* -import xyz.quaver.floatingsearchview.FloatingSearchView -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.floatingsearchview.util.view.MenuView -import xyz.quaver.floatingsearchview.util.view.SearchInputView -import xyz.quaver.pupil.* -import xyz.quaver.pupil.adapters.GalleryBlockAdapter -import xyz.quaver.pupil.databinding.MainActivityBinding -import xyz.quaver.pupil.hitomi.doSearch -import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi -import xyz.quaver.pupil.hitomi.getSuggestionsForQuery -import xyz.quaver.pupil.services.DownloadService -import xyz.quaver.pupil.types.* -import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment -import xyz.quaver.pupil.ui.dialog.GalleryDialog -import xyz.quaver.pupil.ui.view.MainView -import xyz.quaver.pupil.ui.view.ProgressCard -import xyz.quaver.pupil.util.ItemClickSupport -import xyz.quaver.pupil.util.Preferences -import xyz.quaver.pupil.util.requestNotificationPermission -import xyz.quaver.pupil.util.checkUpdate -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.restore -import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog -import java.util.regex.Pattern -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -class MainActivity : - BaseActivity(), - NavigationView.OnNavigationItemSelectedListener -{ - - enum class Mode { - SEARCH, - HISTORY, - DOWNLOAD, - FAVORITE - } - - enum class SortMode { - NEWEST, - POPULAR - } - - private val galleries = ArrayList() - - private var query = "" - set(value) { - field = value - with(findViewById(R.id.search_bar_text)) { - if (text.toString() != value) - setText(query, TextView.BufferType.EDITABLE) - } - } - private var queryStack = mutableListOf() - - private var mode = Mode.SEARCH - private var sortMode = SortMode.NEWEST - - private var galleryIDs: Deferred>? = null - private var totalItems = 0 - private var loadingJob: Job? = null - private var currentPage = 0 - - private lateinit var binding: MainActivityBinding - - private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (!isGranted) { - showNotificationPermissionExplanationDialog(this) - } - } +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import com.google.accompanist.adaptive.calculateDisplayFeatures +import xyz.quaver.pupil.ui.composable.PupilApp +import xyz.quaver.pupil.ui.theme.AppTheme +import xyz.quaver.pupil.ui.viewmodel.MainViewModel +class MainActivity : BaseActivity() { + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) - binding = MainActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - if (intent.action == Intent.ACTION_VIEW) { - intent.dataString?.let { url -> - restore(url, - onFailure = { - Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() - }, onSuccess = { - Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show() - } + val viewModel: MainViewModel by viewModels() + + setContent { + AppTheme { + val windowSize = calculateWindowSizeClass(this) + val displayFeatures = calculateDisplayFeatures(this) + + PupilApp( + windowSize = windowSize, + displayFeatures = displayFeatures, + uiState = viewModel.uiState ) } } - - requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {} - - if (Preferences["download_folder", ""].isEmpty()) - DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] && - ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() } - .contains(Preferences["download_folder", ""]) - ) { - AlertDialog.Builder(this) - .setTitle(R.string.warning) - .setMessage(R.string.unaccessible_download_folder) - .setPositiveButton(android.R.string.ok) { _, _ -> - DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") - }.setNegativeButton(R.string.ignore) { _, _ -> - Preferences["download_folder_ignore_warning"] = true - }.show() - } - - initView() - } - - override fun onResume() { - super.onResume() - - checkUpdate(this) - } - - @OptIn(ExperimentalStdlibApi::class) - override fun onBackPressed() { - when { - binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START) - queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { - query = queryStack.last() - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - else -> super.onBackPressed() - } - } - - override fun onDestroy() { - super.onDestroy() - - (binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - val perPage = Preferences["per_page", "25"].toInt() - val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() - - return when(keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> { - if (currentPage > 0) { - runOnUiThread { - currentPage-- - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - - true - } - KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (currentPage < maxPage) { - runOnUiThread { - currentPage++ - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - - true - } - else -> super.onKeyDown(keyCode, event) - } - } - - private fun initView() { - binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - // -height of the search view < translationY < 0 - binding.contents.searchview.translationY = - min( - max( - binding.contents.searchview.translationY - dy, - -binding.contents.searchview.binding.querySection.root.height.toFloat() - ), 0F) - - if (dy > 0) - binding.contents.fab.hideMenuButton(true) - else if (dy < 0) - binding.contents.fab.showMenuButton(true) - } - }) - - Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) }) - - //NavigationView - binding.navView.setNavigationItemSelectedListener(this) - - with(binding.contents.cancelFab) { - setImageResource(R.drawable.cancel) - setOnClickListener { - DownloadService.cancel(this@MainActivity) - } - } - - with(binding.contents.jumpFab) { - setImageResource(R.drawable.ic_jump) - setOnClickListener { - val perPage = Preferences["per_page", "25"].toInt() - val editText = EditText(context) - - AlertDialog.Builder(context).apply { - setView(editText) - setTitle(R.string.main_jump_title) - setMessage(getString( - R.string.main_jump_message, - currentPage+1, - ceil(totalItems / perPage.toDouble()).roundToInt() - )) - - setPositiveButton(android.R.string.ok) { _, _ -> - currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 - - runOnUiThread { - cancelFetch() - clearGalleries() - loadBlocks() - } - } - }.show() - } - } - - with(binding.contents.randomFab) { - setImageResource(R.drawable.shuffle_variant) - setOnClickListener { - runBlocking { - withTimeoutOrNull(100) { - galleryIDs?.await() - } - }.let { - if (it?.isEmpty() == false) { - val galleryID = it.random() - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - } - } - } - } - - with(binding.contents.idFab) { - setImageResource(R.drawable.numeric) - setOnClickListener { - val editText = EditText(context).apply { - inputType = InputType.TYPE_CLASS_NUMBER - } - - AlertDialog.Builder(context).apply { - setView(editText) - setTitle(R.string.main_open_gallery_by_id) - - setPositiveButton(android.R.string.ok) { _, _ -> - val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - } - }.show() - } - } - - with(binding.contents.view) { - setOnPageTurnListener(object: MainView.OnPageTurnListener { - override fun onPrev(page: Int) { - currentPage-- - - // disable pageturn until the contents are loaded - setCurrentPage(1, false) - - ViewCompat.animate(binding.contents.searchview) - .setDuration(100) - .setInterpolator(DecelerateInterpolator()) - .translationY(0F) - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - - override fun onNext(page: Int) { - currentPage++ - - // disable pageturn until the contents are loaded - setCurrentPage(1, false) - - ViewCompat.animate(binding.contents.searchview) - .setDuration(100) - .setInterpolator(DecelerateInterpolator()) - .translationY(0F) - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - }) - } - - setupSearchBar() - setupRecyclerView() - fetchGalleries(query, sortMode) - loadBlocks() - } - - @SuppressLint("ClickableViewAccessibility") - private fun setupRecyclerView() { - with(binding.contents.recyclerview) { - adapter = GalleryBlockAdapter(galleries).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - onDownloadClickedHandler = { position -> - val galleryID = galleries[position] - - requestNotificationPermission( - this@MainActivity, - requestNotificationPermssionLauncher - ) { - if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress - DownloadService.cancel(this@MainActivity, galleryID) - } - else { - DownloadManager.getInstance(context).addDownloadFolder(galleryID) - DownloadService.download(this@MainActivity, galleryID) - } - } - - closeAllItems() - } - - onDeleteClickedHandler = { position -> - val galleryID = galleries[position] - DownloadService.delete(this@MainActivity, galleryID) - - histories.remove(galleryID) - - if (this@MainActivity.mode != Mode.SEARCH) - runOnUiThread { - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - - closeAllItems() - } - } - ItemClickSupport.addTo(this).apply { - onItemClickListener = listener@{ _, position, v -> - if (v !is ProgressCard) - return@listener - - val intent = Intent(this@MainActivity, ReaderActivity::class.java) - intent.putExtra("galleryID", galleries[position]) - - //TODO: Maybe sprinkling some transitions will be nice :D - startActivity(intent) - } - - onItemLongClickListener = listener@{ _, position, v -> - if (v !is ProgressCard) - return@listener false - - val galleryID = galleries.getOrNull(position) ?: return@listener true - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - - true - } - } - } - } - - private var isFavorite = false - private val defaultSuggestions: List - get() = when { - isFavorite -> { - favoriteTags.map { - TagSuggestion(it.tag, -1, "", it.area ?: "tag") - } + FavoriteHistorySwitch(getString(R.string.search_show_histories)) - } - else -> { - searchHistory.map { - Suggestion(it) - }.takeLast(10) + FavoriteHistorySwitch(getString(R.string.search_show_tags)) - } - }.reversed() - - private var suggestionJob : Job? = null - private fun setupSearchBar() { - with(binding.contents.searchview) { - onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { - override fun onMenuOpened() { - (this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() - } - - override fun onMenuClosed() { - //Do Nothing - } - } - - post { - findViewById(R.id.menu_view).menuItems.firstOrNull { - (it as MenuItem).itemId == R.id.main_menu_thin - }?.let { - (it as MenuItem).isChecked = Preferences["thin"] - } - } - - onHistoryDeleteClickedListener = { - searchHistory.remove(it) - swapSuggestions(defaultSuggestions) - } - onFavoriteHistorySwitchClickListener = { - isFavorite = !isFavorite - swapSuggestions(defaultSuggestions) - } - - onMenuItemClickListener = { - onActionMenuItemSelected(it) - } - - onQueryChangeListener = lambda@{ _, query -> - this@MainActivity.query = query - - suggestionJob?.cancel() - - if (query.isEmpty() or query.endsWith(' ')) { - swapSuggestions(defaultSuggestions) - - return@lambda - } - - swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString()))) - - val currentQuery = query.split(" ").last() - .replace(Regex("^-"), "") - .replace('_', ' ') - - suggestionJob = CoroutineScope(Dispatchers.IO).launch { - val suggestions = kotlin.runCatching { - getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList() - }.getOrElse { mutableListOf() } - - suggestions.filter { - val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}" - favoriteTags.contains(Tag.parse(tag)) - }.reversed().forEach { - suggestions.remove(it) - suggestions.add(0, it) - } - - withContext(Dispatchers.Main) { - swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString()))) - } - } - } - - onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener { - override fun onFocus() { - if (query.isEmpty() or query.endsWith(' ')) - swapSuggestions(defaultSuggestions) - } - - override fun onFocusCleared() { - suggestionJob?.cancel() - - runOnUiThread { - cancelFetch() - clearGalleries() - currentPage = 0 - fetchGalleries(query, sortMode) - loadBlocks() - } - } - } - - attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer) - } - } - - fun onActionMenuItemSelected(item: MenuItem?) { - when(item?.itemId) { - R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) - R.id.main_menu_thin -> { - val thin = !item.isChecked - - item.isChecked = thin - binding.contents.recyclerview.apply { - (adapter as GalleryBlockAdapter).apply { - this.thin = thin - - Preferences["thin"] = thin - } - - adapter = adapter // Force to redraw - } - } - R.id.main_menu_sort_newest -> { - sortMode = SortMode.NEWEST - item.isChecked = true - - runOnUiThread { - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - R.id.main_menu_sort_popular -> { - sortMode = SortMode.POPULAR - item.isChecked = true - - runOnUiThread { - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - } - } - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - runOnUiThread { - binding.drawer.closeDrawers() - - when(item.itemId) { - R.id.main_drawer_home -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.SEARCH - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_history -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.HISTORY - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_downloads -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.DOWNLOAD - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_favorite -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.FAVORITE - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_help -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) - } - R.id.main_drawer_github -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) - } - R.id.main_drawer_homepage -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page)))) - } - R.id.main_drawer_email -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) - } - R.id.main_drawer_kakaotalk -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord)))) - } - } - } - - return true - } - - private fun cancelFetch() { - galleryIDs?.cancel() - loadingJob?.cancel() - } - - private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch { - galleries.clear() - - with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) { - this ?: return@with - - this.notifyDataSetChanged() - } - - binding.contents.noresult.visibility = View.INVISIBLE - binding.contents.progressbar.show() - } - - private fun fetchGalleries(query: String, sortMode: SortMode) { - val defaultQuery: String = Preferences["default_query"] - - if (query.isNotBlank()) - searchHistory.add(query) - - if (query != queryStack.lastOrNull()) { - queryStack.remove(query) - queryStack.add(query) - } - - if (query.isNotEmpty() && mode != Mode.SEARCH) { - Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply { - setAction(android.R.string.ok) { - cancelFetch() - clearGalleries() - currentPage = 0 - mode = Mode.SEARCH - queryStack.clear() - fetchGalleries(query, sortMode) - loadBlocks() - } - }.show() - } - - galleryIDs = null - - if (galleryIDs?.isActive == true) - return - - galleryIDs = CoroutineScope(Dispatchers.IO).async { - when(mode) { - Mode.SEARCH -> { - when { - query.isEmpty() and defaultQuery.isEmpty() -> { - when(sortMode) { - SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") - else -> getGalleryIDsFromNozomi(null, "index", "all") - }.also { - totalItems = it.size - } - } - else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also { - totalItems = it.size - } - } - } - Mode.HISTORY -> { - when { - query.isEmpty() -> { - histories.reversed().also { - totalItems = it.size - } - } - else -> { - val result = doSearch(query).sorted() - histories.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - Mode.DOWNLOAD -> { - val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList() - - when { - query.isEmpty() -> downloads.reversed().also { - totalItems = it.size - } - else -> { - val result = doSearch(query).sorted() - downloads.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - Mode.FAVORITE -> { - when { - query.isEmpty() -> favorites.reversed().also { - totalItems = it.size - } - else -> { - val result = doSearch(query).sorted() - favorites.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - }.toList() - } - } - - private fun loadBlocks() { - val perPage = Preferences["per_page", "25"].toInt() - - loadingJob = CoroutineScope(Dispatchers.IO).launch { - val galleryIDs = try { - galleryIDs!!.await().also { - if (it.isEmpty()) - throw Exception("No result") - } - } catch (e: Exception) { - if (e !is CancellationException) - FirebaseCrashlytics.getInstance().recordException(e) - - withContext(Dispatchers.Main) { - binding.contents.noresult.visibility = View.VISIBLE - binding.contents.progressbar.hide() - } - - return@launch - } - - launch(Dispatchers.Main) { - binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage) - } - - galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks -> - for (chunk in chunks) - chunk.map { galleryID -> - async { - Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let { - galleryID - } - } - }.forEach { - it.await()?.also { - withContext(Dispatchers.Main) { - binding.contents.progressbar.hide() - - galleries.add(it) - binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1) - } - } - } - } - } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt new file mode 100644 index 00000000..00339db0 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class ContentType { + SINGLE_PANE, DUAL_PANE +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt new file mode 100644 index 00000000..b4fdcb75 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt @@ -0,0 +1,34 @@ +package xyz.quaver.pupil.ui.composable + +import android.graphics.Rect +import androidx.window.layout.FoldingFeature +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +sealed interface DevicePosture { + data object NormalPosture: DevicePosture + + data class BookPosture( + val hingePosition: Rect + ): DevicePosture + + data class Separating( + val hingePosition: Rect, + val orientation: FoldingFeature.Orientation + ): DevicePosture +} + +@OptIn(ExperimentalContracts::class) +fun isBookPosture(foldingFeature: FoldingFeature?): Boolean { + contract { returns(true) implies (foldingFeature != null) } + + return foldingFeature?.state == FoldingFeature.State.HALF_OPENED && + foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL +} + +@OptIn(ExperimentalContracts::class) +fun isSeparating(foldingFeature: FoldingFeature?): Boolean { + contract { returns(true) implies (foldingFeature != null) } + + return foldingFeature?.state == FoldingFeature.State.FLAT && foldingFeature.isSeparating +} \ No newline at end of file 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 new file mode 100644 index 00000000..7b13c1af --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt @@ -0,0 +1,92 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.PermanentNavigationDrawer +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.compose.rememberNavController +import androidx.window.layout.DisplayFeature +import androidx.window.layout.FoldingFeature +import xyz.quaver.pupil.ui.viewmodel.MainUIState + +@Composable +fun PupilApp( + windowSize: WindowSizeClass, + displayFeatures: List, + uiState: MainUIState +) { + val navigationType: NavigationType + val contentType: ContentType + + val foldingFeature: FoldingFeature? = displayFeatures.filterIsInstance().firstOrNull() + val foldingDevicePosture = when { + isBookPosture(foldingFeature) -> DevicePosture.BookPosture(foldingFeature.bounds) + isSeparating(foldingFeature) -> DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) + else -> DevicePosture.NormalPosture + } + + when (windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = ContentType.SINGLE_PANE + } + WindowWidthSizeClass.Medium -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) { + ContentType.DUAL_PANE + } else { + ContentType.SINGLE_PANE + } + } + WindowWidthSizeClass.Expanded -> { + navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) { + NavigationType.NAVIGATION_RAIL + } else { + NavigationType.PERMANENT_NAVIGATION_DRAWER + } + contentType = ContentType.DUAL_PANE + } + else -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = ContentType.SINGLE_PANE + } + } + + val navigationContentPosition = when (windowSize.heightSizeClass) { + WindowHeightSizeClass.Compact -> NavigationContentPosition.TOP + WindowHeightSizeClass.Medium, + WindowHeightSizeClass.Expanded -> NavigationContentPosition.CENTER + else -> NavigationContentPosition.TOP + } + + PupilNavigationWrapper( + navigationType, + contentType, + navigationContentPosition + ) + +} + +@Composable +private fun PupilNavigationWrapper( + navigationType: NavigationType, + contentType: ContentType, + navigationContentPosition: NavigationContentPosition +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val coroutineScope = rememberCoroutineScope() + + if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) { + PermanentNavigationDrawer(drawerContent = { + PermanentNavigationDrawerContent( + navigationContentPosition = navigationContentPosition + ) + }) { +// PupilMain() + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..d32c2f31 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt @@ -0,0 +1,38 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.ui.graphics.vector.ImageVector +import xyz.quaver.pupil.R + +data class MainDestination( + val route: String, + val icon: ImageVector, + val textId: Int +) + +val mainDestinations = listOf( + MainDestination( + "search", + Icons.Default.Search, + R.string.main_destination_search + ), + MainDestination( + "history", + Icons.Default.History, + R.string.main_destination_history + ), + MainDestination( + "downloads", + Icons.Default.Download, + R.string.main_destination_downloads + ), + MainDestination( + "favorites", + Icons.Default.Star, + R.string.main_destination_favorites + ), +) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt new file mode 100644 index 00000000..1b10b675 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class NavigationContentPosition { + TOP, CENTER +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt new file mode 100644 index 00000000..2513c175 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt @@ -0,0 +1,59 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PermanentDrawerSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import xyz.quaver.pupil.R + +@Composable +fun PermanentNavigationDrawerContent( + navigationContentPosition: NavigationContentPosition +) { + PermanentDrawerSheet( + modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp), + drawerContainerColor = MaterialTheme.colorScheme.inverseOnSurface + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.inverseOnSurface) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(R.drawable.app_icon), + tint = Color.Unspecified, + contentDescription = "app icon" + ) + Text( + modifier = Modifier.padding(16.dp), + text = "Pupil", + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.primary + ) + } + } + Column( + + ) { + Text("Help") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt new file mode 100644 index 00000000..73796fab --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class NavigationType { + NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt deleted file mode 100644 index 7509ea70..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt +++ /dev/null @@ -1,255 +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.ui.dialog - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout.LayoutParams -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.quaver.pupil.hitomi.Gallery -import xyz.quaver.pupil.hitomi.getGallery -import xyz.quaver.pupil.R -import xyz.quaver.pupil.adapters.GalleryBlockAdapter -import xyz.quaver.pupil.adapters.ThumbnailPageAdapter -import xyz.quaver.pupil.databinding.* -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.ui.ReaderActivity -import xyz.quaver.pupil.ui.view.TagChip -import xyz.quaver.pupil.util.ItemClickSupport -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.wordCapitalize -import java.util.* -import kotlin.collections.ArrayList - -class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) { - - val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() - - private lateinit var binding: GalleryDialogBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = GalleryDialogBinding.inflate(layoutInflater) - setContentView(binding.root) - - window?.attributes.apply { - this ?: return@apply - - width = LayoutParams.MATCH_PARENT - height = LayoutParams.MATCH_PARENT - } - - with(binding.fab) { - setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right)) - setOnClickListener { - context.startActivity(Intent(context, ReaderActivity::class.java).apply { - putExtra("galleryID", galleryID) - }) - } - } - - CoroutineScope(Dispatchers.IO).launch { - try { - val gallery = getGallery(galleryID) - - launch (Dispatchers.Main) { - binding.progressbar.visibility = View.GONE - binding.title.text = gallery.title - binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } - - with(binding.type) { - text = gallery.type.wordCapitalize() - setOnClickListener { - gallery.type.let { - when (it) { - "artist CG" -> "artistcg" - "game CG" -> "gamecg" - else -> it - } - }.let { - onChipClickedHandler.forEach { handler -> - handler.invoke(Tag("type", it)) - } - } - } - } - - binding.cover.showImage(Uri.parse(gallery.cover)) - - addDetails(gallery) - addThumbnails(gallery) - addRelated(gallery) - } - } catch (e: Exception) { - Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply { - if (Locale.getDefault().language == "ko") - setAction(context.getText(R.string.https_text)) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https)))) - } - }.show() - } - } - } - - private fun addDetails(gallery: Gallery) { - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_details) - - listOf( - R.string.gallery_artists, - R.string.gallery_groups, - R.string.gallery_language, - R.string.gallery_series, - R.string.gallery_characters, - R.string.gallery_tags - ).zip( - listOf( - gallery.artists.map { Tag("artist", it) }, - gallery.groups.map { Tag("group", it) }, - listOf(gallery.language).map { Tag("language", it) }, - gallery.series.map { Tag("series", it) }, - gallery.characters.map { Tag("character", it) }, - gallery.tags.sortedBy { - val tag = Tag.parse(it) - - if (favoriteTags.contains(tag)) - -1 - else - when(Tag.parse(it).area) { - "female" -> 0 - "male" -> 1 - else -> 2 - } - }.map { - Tag.parse(it).let { tag -> - when { - tag.area != null -> tag - else -> Tag("tag", it) - } - } - } - ) - ).filter { - (_, content) -> content.isNotEmpty() - }.forEach { (title, content) -> - GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply { - type.setText(title) - - content.forEach { tag -> - tags.addView( - TagChip(context, tag).apply { - setOnClickListener { - onChipClickedHandler.forEach { handler -> - handler.invoke(tag) - } - } - } - ) - } - } - } - } - } - - private fun addThumbnails(gallery: Gallery) { - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_thumbnails) - - val pager = ViewPager2(context).apply { - adapter = ThumbnailPageAdapter(gallery.thumbnails) - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - } - - contents.addView( - pager, - LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - ) - - // TODO: Change to direct allocation - GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply { - dotindicator.setViewPager2(pager) - } - } - } - - private fun addRelated(gallery: Gallery) { - val galleries = mutableListOf() - - val adapter = GalleryBlockAdapter(galleries).apply { - onChipClickedHandler.add { tag -> - this@GalleryDialog.onChipClickedHandler.forEach { handler -> - handler.invoke(tag) - } - } - } - - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_related) - - contents.addView(RecyclerView(context).apply { - layoutManager = LinearLayoutManager(context) - this.adapter = adapter - - ItemClickSupport.addTo(this).apply { - onItemClickListener = { _, position, _ -> - context.startActivity(Intent(context, ReaderActivity::class.java).apply { - putExtra("galleryID", galleries[position]) - }) - } - onItemLongClickListener = { _, position, _ -> - GalleryDialog(context, galleries[position]).apply { - onChipClickedHandler.add { tag -> - this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) } - } - }.show() - - true - } - } - }) - - CoroutineScope(Dispatchers.IO).launch { - gallery.related.forEach { galleryID -> - Cache.getInstance(context, galleryID).getGalleryBlock()?.let { - galleries.add(galleryID) - } - } - - withContext(Dispatchers.Main) { - adapter.notifyDataSetChanged() - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt deleted file mode 100644 index b4fbf5b6..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt +++ /dev/null @@ -1,263 +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.ui.fragment - -import android.graphics.ColorFilter -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.swiperefreshlayout.widget.CircularProgressDrawable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import xyz.quaver.io.FileX -import xyz.quaver.io.SAFileX -import xyz.quaver.io.util.deleteRecursively -import xyz.quaver.io.util.getChild -import xyz.quaver.io.util.readText -import xyz.quaver.io.util.writeText -import xyz.quaver.pupil.R -import xyz.quaver.pupil.histories -import xyz.quaver.pupil.hitomi.json -import xyz.quaver.pupil.util.byteToString -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.downloader.Metadata -import java.io.File -import kotlin.math.roundToInt - -class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { - - private var job: Job? = null - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey) - - initPreferences() - } - - override fun onPreferenceClick(preference: Preference): Boolean { - val context = context ?: return false - - with(preference) { - when (key) { - "delete_cache" -> { - val dir = File(context.cacheDir, "imageCache") - - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_cache_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - if (dir.exists()) - dir.deleteRecursively() - - Cache.instances.clear() - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - "recover_downloads" -> { - val density = context.resources.displayMetrics.density - this.icon = object: CircularProgressDrawable(context) { - override fun getIntrinsicHeight() = (24*density).roundToInt() - override fun getIntrinsicWidth() = (24*density).roundToInt() - }.apply { - setStyle(CircularProgressDrawable.DEFAULT) - colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN) - start() - } - - val downloadManager = DownloadManager.getInstance(context) - - val downloadFolderMap = downloadManager.downloadFolderMap - - downloadFolderMap.clear() - - downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder -> - val metadataFile = FileX(context, folder, ".metadata") - - if (!metadataFile.exists()) return@forEach - - val metadata = metadataFile.readText()?.let { - runCatching { - json.decodeFromString(it) - }.getOrNull() - } ?: return@forEach - - val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach - - downloadFolderMap[galleryID] = folder.name - } - - downloadManager.downloadFolderMap.putAll(downloadFolderMap) - val downloads = FileX(context, downloadManager.downloadFolder, ".download") - - if (!downloads.exists()) downloads.createNewFile() - downloads.writeText(Json.encodeToString(downloadFolderMap)) - - this.icon = null - Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show() - } - "delete_downloads" -> { - val dir = DownloadManager.getInstance(context).downloadFolder - - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_downloads_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - CoroutineScope(Dispatchers.IO).launch { - job?.cancel() - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage_loading) - } - - if (dir.exists()) - dir.listFiles()?.forEach { - when (it) { - is FileX -> it.deleteRecursively() - else -> it.deleteRecursively() - } - } - - job = launch { - var size = 0L - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - } - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - "clear_history" -> { - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_history_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - histories.clear() - summary = context.getString(R.string.settings_clear_history_summary, histories.size) - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - else -> return false - } - } - - return true - } - - private fun initPreferences() { - val context = context ?: return - - with(findPreference("delete_cache")) { - this ?: return@with - - val dir = File(context.cacheDir, "imageCache") - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("delete_downloads")) { - this ?: return@with - - val dir = DownloadManager.getInstance(context).downloadFolder - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - job?.cancel() - job = CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - - size += it.length() - } - } - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("clear_history")) { - this ?: return@with - - summary = context.getString(R.string.settings_clear_history_summary, histories.size) - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("recover_downloads")) { - this ?: return@with - - onPreferenceClickListener = this@ManageStorageFragment - } - } - - override fun onDestroy() { - job?.cancel() - super.onDestroy() - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt new file mode 100644 index 00000000..802b4d5f --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt @@ -0,0 +1,67 @@ +package xyz.quaver.pupil.ui.theme +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFF006688) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFC2E8FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001E2B) +val md_theme_light_secondary = Color(0xFF4E616D) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFD1E5F3) +val md_theme_light_onSecondaryContainer = Color(0xFF091E28) +val md_theme_light_tertiary = Color(0xFF5F5A7D) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFE5DEFF) +val md_theme_light_onTertiaryContainer = Color(0xFF1C1736) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFBFCFE) +val md_theme_light_onBackground = Color(0xFF191C1E) +val md_theme_light_surface = Color(0xFFFBFCFE) +val md_theme_light_onSurface = Color(0xFF191C1E) +val md_theme_light_surfaceVariant = Color(0xFFDCE3E9) +val md_theme_light_onSurfaceVariant = Color(0xFF40484D) +val md_theme_light_outline = Color(0xFF71787D) +val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3) +val md_theme_light_inverseSurface = Color(0xFF2E3133) +val md_theme_light_inversePrimary = Color(0xFF75D1FF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF006688) +val md_theme_light_outlineVariant = Color(0xFFC0C7CD) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFF75D1FF) +val md_theme_dark_onPrimary = Color(0xFF003548) +val md_theme_dark_primaryContainer = Color(0xFF004D67) +val md_theme_dark_onPrimaryContainer = Color(0xFFC2E8FF) +val md_theme_dark_secondary = Color(0xFFB5C9D7) +val md_theme_dark_onSecondary = Color(0xFF20333D) +val md_theme_dark_secondaryContainer = Color(0xFF364954) +val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F3) +val md_theme_dark_tertiary = Color(0xFFC9C2EA) +val md_theme_dark_onTertiary = Color(0xFF312C4C) +val md_theme_dark_tertiaryContainer = Color(0xFF474364) +val md_theme_dark_onTertiaryContainer = Color(0xFFE5DEFF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF191C1E) +val md_theme_dark_onBackground = Color(0xFFE1E2E5) +val md_theme_dark_surface = Color(0xFF191C1E) +val md_theme_dark_onSurface = Color(0xFFE1E2E5) +val md_theme_dark_surfaceVariant = Color(0xFF40484D) +val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD) +val md_theme_dark_outline = Color(0xFF8A9297) +val md_theme_dark_inverseOnSurface = Color(0xFF191C1E) +val md_theme_dark_inverseSurface = Color(0xFFE1E2E5) +val md_theme_dark_inversePrimary = Color(0xFF006688) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF75D1FF) +val md_theme_dark_outlineVariant = Color(0xFF40484D) +val md_theme_dark_scrim = Color(0xFF000000) + + +val seed = Color(0xFF4FC3F7) diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt new file mode 100644 index 00000000..9fe02e7a --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt @@ -0,0 +1,90 @@ +package xyz.quaver.pupil.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun AppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt deleted file mode 100644 index 93fea583..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt +++ /dev/null @@ -1,216 +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.ui.view - -import android.content.Context -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.drawable.Animatable -import android.text.Editable -import android.text.TextWatcher -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.swiperefreshlayout.widget.CircularProgressDrawable -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import xyz.quaver.floatingsearchview.FloatingSearchView -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.floatingsearchview.util.view.SearchInputView -import xyz.quaver.pupil.R -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.* -import java.util.* - -class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FloatingSearchView(context, attrs), - FloatingSearchView.OnSearchListener, - TextWatcher -{ - private val searchInputView = findViewById(R.id.search_bar_text) - - var onHistoryDeleteClickedListener: ((String) -> Unit)? = null - var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null - - init { - searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions - - searchInputView.addTextChangedListener(this) - onSearchListener = this - onBindSuggestionCallback = { binding, item, itemPosition -> - onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition) - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - - } - - override fun afterTextChanged(s: Editable?) { - s ?: return - - if (s.any { it.isUpperCase() }) - s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault())) - } - - override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { - when (searchSuggestion) { - is TagSuggestion -> { - val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}" - with(searchInputView.text!!) { - delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length) - - if (!this.contains(tag)) - append("$tag ") - } - } - is Suggestion -> { - with(searchInputView.text!!) { - clear() - append(searchSuggestion.body) - } - } - is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke() - } - } - - override fun onSearchAction(currentQuery: String?) {} - - fun onBindSuggestion( - suggestionView: View?, - leftIcon: ImageView?, - textView: TextView?, - item: SearchSuggestion?, - itemPosition: Int - ) { - when(item) { - is TagSuggestion -> { - val tag = "${item.n}:${item.s}" - - leftIcon?.setImageDrawable( - ResourcesCompat.getDrawable( - resources, - when(item.n) { - "female" -> R.drawable.gender_female - "male" -> R.drawable.gender_male - "language" -> R.drawable.translate - "group" -> R.drawable.account_group - "character" -> R.drawable.account_star - "series" -> R.drawable.book_open - "artist" -> R.drawable.brush - else -> R.drawable.tag - }, - context.theme) - ) - - with(suggestionView?.findViewById(R.id.right_icon)) { - this ?: return@with - - if (favoriteTags.contains(Tag.parse(tag))) - setImageResource(R.drawable.ic_star_filled) - else - setImageResource(R.drawable.ic_star_empty) - - visibility = View.VISIBLE - rotation = 0f - - isEnabled = true - isClickable = true - - setOnClickListener { - val tag = Tag.parse(tag) - - if (favoriteTags.contains(tag)) { - setImageResource(R.drawable.ic_star_empty) - favoriteTags.remove(tag) - } - else { - setImageDrawable( - AnimatedVectorDrawableCompat.create(context, - R.drawable.avd_star - )) - (drawable as Animatable).start() - - favoriteTags.add(tag) - } - } - } - - if (item.t > 0) { - (suggestionView as? LinearLayout)?.let { - val count = it.findViewById(R.id.count) - if (count == null) - it.addView( - LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false) - .apply { - this as TextView - - text = item.t.toString() - }, 2 - ) - else - count.text = item.t.toString() - } - } - } - is FavoriteHistorySwitch -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme)) - } - is Suggestion -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme)) - - with(suggestionView?.findViewById(R.id.right_icon)) { - this ?: return@with - - setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme)) - - visibility = View.VISIBLE - rotation = 0f - - isEnabled = true - isClickable = true - - setOnClickListener { - onHistoryDeleteClickedListener?.invoke(item.body) - } - } - } - is LoadingSuggestion -> { - leftIcon?.setImageDrawable(CircularProgressDrawable(context).also { - it.setStyle(CircularProgressDrawable.DEFAULT) - it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN) - it.start() - }) - } - is NoResultSuggestion -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme)) - } - } - } -} diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java b/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java deleted file mode 100644 index 2c37eb83..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java +++ /dev/null @@ -1,462 +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.ui.view; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.os.Vibrator; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.view.NestedScrollingChild; -import androidx.core.view.NestedScrollingChildHelper; -import androidx.core.view.NestedScrollingParent; -import androidx.core.view.NestedScrollingParentHelper; -import androidx.core.view.ViewCompat; -import androidx.core.widget.TextViewCompat; - -import xyz.quaver.pupil.R; - -@SuppressWarnings("NullableProblems") -public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent { - - private static final int PAGE_TURN_LAYOUT_SIZE = 48; - private static final int PAGE_TURN_ANIM_DURATION = 500; - private static final int PREV_OFFSET = 64; - private static final int RIPPLE_GIVE = 4; - - private final float adjustedPageTurnLayoutSize; - private final float adjustedPrevOffset; - private final float adjustedRippleGive; - - final private NestedScrollingParentHelper mNestedScrollingParentHelper; - final private NestedScrollingChildHelper mNestedScrollingChildHelper; - - final private Vibrator mVibrator; - - private View mTarget; - - private TextView mPrev; - private TextView mNext; - - private final Paint mRipplePaint = new Paint(); - private final Rect mRippleBound = new Rect(); - - private int mRippleSize = 0; - private final int mRippleTargetSize; - private final ValueAnimator mRippleAnimator = new ValueAnimator(); - - private int mCurrentOverScroll = 0; - - private int mCurrentPage = 1; - private boolean mShowPrev; - private boolean mShowNext; - - private OnPageTurnListener mOnPageTurnListener; - - public MainView(@NonNull Context context) { - this(context, null); - } - - public MainView(@NonNull Context context, AttributeSet attr) { - this(context, attr, 0); - } - - public MainView(@NonNull Context context, AttributeSet attr, int defStyle) { - super(context, attr, defStyle); - - setWillNotDraw(false); - - DisplayMetrics metrics = getResources().getDisplayMetrics(); - - adjustedPageTurnLayoutSize = PAGE_TURN_LAYOUT_SIZE * metrics.density; - adjustedPrevOffset = PREV_OFFSET * metrics.density; - adjustedRippleGive = RIPPLE_GIVE * metrics.density; - - mRippleTargetSize = metrics.widthPixels; - - mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); - mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); - - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - - mRippleAnimator.addUpdateListener(animation -> { - mRippleSize = (int) animation.getAnimatedValue(); - invalidate(); - }); - mRippleAnimator.setDuration(PAGE_TURN_ANIM_DURATION); - - initPageTurnView(); - } - - public void setCurrentPage(int currentPage, boolean showNext) { - mCurrentPage = currentPage; - - mShowPrev = currentPage > 1; - mShowNext = showNext; - - mPrev.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage-1)); - mNext.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage+1)); - } - - public void setOnPageTurnListener(OnPageTurnListener listener) { - mOnPageTurnListener = listener; - } - - private void initPageTurnView() { - TextView prev = new TextView(getContext()); - TextView next = new TextView(getContext()); - - prev.setGravity(Gravity.CENTER_VERTICAL); - next.setGravity(Gravity.CENTER_VERTICAL); - - prev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.navigate_prev, 0, 0, 0); - next.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.navigate_next, 0); - - TextViewCompat.setCompoundDrawableTintList(prev, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent)); - TextViewCompat.setCompoundDrawableTintList(next, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent)); - - prev.setVisibility(View.INVISIBLE); - next.setVisibility(View.INVISIBLE); - - mPrev = prev; - mNext = next; - - addView(mPrev); - addView(mNext); - - setCurrentPage(1, false); - } - - private void ensureTarget() { - if (mTarget == null) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - - if (!child.equals(mNext) && !child.equals(mPrev)) { - mTarget = child; - break; - } - } - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final int width = getMeasuredWidth(); - final int height = getMeasuredHeight(); - - if (getChildCount() == 0) - return; - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mTarget.layout( - getPaddingLeft(), - getPaddingTop(), - width - getPaddingRight(), - height - getPaddingBottom() - ); - - final int prevWidth = mPrev.getMeasuredWidth(); - mPrev.layout( - width / 2 - prevWidth / 2, - getPaddingTop() + (int) adjustedPrevOffset, - width / 2 + prevWidth / 2, - getPaddingTop() + (int) adjustedPrevOffset + mPrev.getMeasuredHeight() - ); - - final int nextWidth = mNext.getMeasuredWidth(); - mNext.layout( - width / 2 - nextWidth / 2, - height - getPaddingBottom() - mNext.getMeasuredHeight(), - width / 2 + nextWidth / 2, - height - getPaddingBottom() - ); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mTarget.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY) - ); - - mPrev.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY) - ); - - mNext.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY) - ); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mCurrentOverScroll == 0) - return; - - if (mCurrentOverScroll > 0) { - mRippleBound.set( - getPaddingLeft(), - (int) (getPaddingTop() - adjustedRippleGive), - getMeasuredWidth() - getPaddingRight(), - (int) (getPaddingTop() + adjustedPrevOffset + mPrev.getMeasuredHeight() + adjustedRippleGive) - ); - } - - if (mCurrentOverScroll < 0) { - final int height = getMeasuredHeight(); - mRippleBound.set( - getPaddingLeft(), - (int) (height - getPaddingBottom() - mNext.getMeasuredHeight() - adjustedRippleGive), - getMeasuredWidth() - getPaddingRight(), - height - getPaddingBottom() - ); - } - - mRipplePaint.reset(); - mRipplePaint.setStyle(Paint.Style.FILL); - - int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - switch (currentNightMode) { - case Configuration.UI_MODE_NIGHT_YES: - mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_700)); - break; - case Configuration.UI_MODE_NIGHT_NO: - mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_300)); - break; - } - - canvas.drawCircle( - (mRippleBound.left + mRippleBound.right) / 2F, - mCurrentOverScroll > 0 ? mRippleBound.bottom : mRippleBound.top, - mRippleSize, - mRipplePaint - ); - } - - private void onOverscroll(int overscroll) { - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mCurrentOverScroll = overscroll; - - if (overscroll > 0) { - mPrev.setVisibility(View.VISIBLE); - mNext.setVisibility(View.INVISIBLE); - } else if (overscroll < 0) { - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.VISIBLE); - } else { - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.INVISIBLE); - } - - if (Math.abs(overscroll) >= adjustedPageTurnLayoutSize) { - if (!mRippleAnimator.isStarted() && mRippleSize != mRippleTargetSize) { - mVibrator.vibrate(10); - - mRippleAnimator.setIntValues(mRippleSize, mRippleTargetSize); - mRippleAnimator.start(); - } - } else { - if (!mRippleAnimator.isStarted() && mRippleSize != 0) { - mRippleAnimator.setIntValues(mRippleSize, 0); - mRippleAnimator.start(); - } - } - - float clippedOverScrollTop = (overscroll > 0 ? 1 : -1) * Math.min(Math.abs(overscroll), adjustedPageTurnLayoutSize); - mTarget.setTranslationY(clippedOverScrollTop); - } - - private void onOverscrollEnd(int overscroll) { - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mRippleAnimator.cancel(); - mRippleAnimator.setIntValues(mRippleSize, 0); - mRippleAnimator.start(); - - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.INVISIBLE); - - ViewCompat.animate(mTarget) - .setDuration(PAGE_TURN_ANIM_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .translationY(0); - - if (Math.abs(overscroll) > adjustedPageTurnLayoutSize && mOnPageTurnListener != null) { - if (overscroll > 0) - mOnPageTurnListener.onPrev(mCurrentPage-1); - if (overscroll < 0) - mOnPageTurnListener.onNext(mCurrentPage+1); - } - } - - // NestedScrollingParent - - private int mTotalUnconsumed = 0; - - @Override - public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { - return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; - } - - @Override - public void onNestedScrollAccepted(View child, View target, int axes) { - mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); - startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); - - mTotalUnconsumed = 0; - } - - @Override - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { - if (mTotalUnconsumed != 0 && dy > 0 == mTotalUnconsumed > 0) { - if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) { - consumed[1] = dy - mTotalUnconsumed; - mTotalUnconsumed = 0; - } else { - mTotalUnconsumed -= dy; - consumed[1] = dy; - } - - onOverscroll(mTotalUnconsumed); - } - - final int[] parentConsumed = new int[2]; - if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { - consumed[0] += parentConsumed[0]; - consumed[1] += parentConsumed[1]; - } - } - - @Override - public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { - final int[] mParentOffsetInWindow = new int[2]; - dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow); - - final int dy = dyUnconsumed + mParentOffsetInWindow[1]; - - if (mTotalUnconsumed == 0 && ((dy < 0 && !mShowPrev) || (dy > 0 && !mShowNext))) - return; - - if (dy != 0) { - mTotalUnconsumed -= dy; - onOverscroll(mTotalUnconsumed); - } - } - - @Override - public void onStopNestedScroll(View child) { - mNestedScrollingParentHelper.onStopNestedScroll(child); - - if (Math.abs(mTotalUnconsumed) > 0) { - onOverscrollEnd(mTotalUnconsumed); - mTotalUnconsumed = 0; - } - - stopNestedScroll(); - } - - // NestedScrollingChild - - @Override - public void setNestedScrollingEnabled(boolean enabled) { - mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); - } - - @Override - public boolean isNestedScrollingEnabled() { - return mNestedScrollingChildHelper.isNestedScrollingEnabled(); - } - - @Override - public boolean startNestedScroll(int axes) { - return mNestedScrollingChildHelper.startNestedScroll(axes); - } - - @Override - public void stopNestedScroll() { - mNestedScrollingChildHelper.stopNestedScroll(); - } - - @Override - public boolean hasNestedScrollingParent() { - return mNestedScrollingChildHelper.hasNestedScrollingParent(); - } - - @Override - public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { - return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); - } - - @Override - public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { - return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); - } - - @Override - public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { - return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); - } - - @Override - public boolean dispatchNestedPreFling(float velocityX, float velocityY) { - return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); - } - - public interface OnPageTurnListener { - void onPrev(int page); - void onNext(int page); - } -} diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt deleted file mode 100644 index 0b5a32b1..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.quaver.pupil.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.cardview.widget.CardView -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import xyz.quaver.pupil.R -import xyz.quaver.pupil.databinding.ProgressCardViewBinding - -class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) { - - enum class Type { - LOADING, - CACHE, - DOWNLOAD - } - - var type: Type = Type.LOADING - set(value) { - field = value - - when (field) { - Type.LOADING -> R.color.colorAccent - Type.CACHE -> R.color.material_blue_700 - Type.DOWNLOAD -> R.color.material_green_a700 - }.let { - val color = ContextCompat.getColor(context, it) - DrawableCompat.setTint(binding.progressbar.progressDrawable, color) - } - } - - var progress: Int - get() = binding.progressbar.progress - set(value) { - binding.progressbar.progress = value - } - var max: Int - get() = binding.progressbar.max - set(value) { - binding.progressbar.max = value - - binding.progressbar.visibility = - if (value == 0) - GONE - else - VISIBLE - } - - val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this) - - init { - binding.content.setOnClickListener { - performClick() - } - - binding.content.setOnLongClickListener { - performLongClick() - } - } - - override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { - if (childCount == 0) - super.addView(child, index, params) - else - binding.content.addView(child, index, params) - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt deleted file mode 100644 index 917979da..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view - -import android.annotation.SuppressLint -import android.content.Context -import androidx.core.content.ContextCompat -import com.google.android.material.chip.Chip -import xyz.quaver.pupil.R -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.util.translations -import xyz.quaver.pupil.util.wordCapitalize - -@SuppressLint("ViewConstructor") -class TagChip(context: Context, _tag: Tag) : Chip(context) { - - val tag: Tag = - _tag.let { - when { - it.area != null -> it - else -> Tag("tag", _tag.tag) - } - } - - private val languages = context.resources.getStringArray(R.array.languages).map { - it.split("|").let { split -> - Pair(split[0], split[1]) - } - }.toMap() - - init { - when(tag.area) { - "male" -> { - setChipBackgroundColorResource(R.color.material_blue_700) - setTextColor(ContextCompat.getColor(context, android.R.color.white)) - setCloseIconTintResource(android.R.color.white) - chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white) - } - "female" -> { - setChipBackgroundColorResource(R.color.material_pink_600) - setTextColor(ContextCompat.getColor(context, android.R.color.white)) - setCloseIconTintResource(android.R.color.white) - chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white) - } - } - - if (favoriteTags.contains(tag)) - setChipBackgroundColorResource(R.color.material_orange_500) - - isCloseIconVisible = true - closeIcon = ContextCompat.getDrawable(context, - if (favoriteTags.contains(tag)) - R.drawable.ic_star_filled - else - R.drawable.ic_star_empty - ) - - setOnCloseIconClickListener { - if (favoriteTags.contains(tag)) { - favoriteTags.remove(tag) - closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty) - - when(tag.area) { - "male" -> setChipBackgroundColorResource(R.color.material_blue_700) - "female" -> setChipBackgroundColorResource(R.color.material_pink_600) - else -> chipBackgroundColor = null - } - } else { - favoriteTags.add(tag) - closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled) - setChipBackgroundColorResource(R.color.material_orange_500) - } - } - - text = when (tag.area) { - "language" -> languages[tag.tag] - else -> (translations[tag.tag] ?: tag.tag).wordCapitalize() - } - - setEnsureMinTouchTargetSize(false) - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt deleted file mode 100644 index 3c3dd136..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view - -import android.content.Context -import android.content.res.TypedArray -import android.util.AttributeSet -import android.util.Log -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import kotlinx.coroutines.* -import xyz.quaver.pupil.R -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.types.Tags - -class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet by tags { - - object Defaults { - const val maxChipSize = 10 - } - - var maxChipSize: Int = Defaults.maxChipSize - set(value) { - field = value - - refresh() - } - - private val moreView = Chip(context).apply { - text = "…" - - setEnsureMinTouchTargetSize(false) - - setOnClickListener { - removeView(this) - - for (i in maxChipSize until tags.size) { - val tag = tags.elementAt(i) - - addView(TagChip(context, tag).apply { - setOnClickListener { - onClickListener?.invoke(tag) - } - }) - } - } - } - - var onClickListener: ((Tag) -> Unit)? = null - - private fun applyAttributes(attr: TypedArray) { - maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize) - } - - private var refreshJob: Job? = null - fun refresh() { - refreshJob?.cancel() - this.removeAllViews() - - refreshJob = CoroutineScope(Dispatchers.Main).launch { - tags.take(maxChipSize).map { - CoroutineScope(Dispatchers.Default).async { - TagChip(context, it).apply { - setOnClickListener { - onClickListener?.invoke(this.tag) - } - } - } - }.forEach { - addView(it.await()) - } - - if (maxChipSize > 0 && tags.size > maxChipSize) - addView(moreView) - } - } - - init { - applyAttributes(context.obtainStyledAttributes(attr, R.styleable.TagChipGroup)) - - refresh() - } - -} \ No newline at end of file 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 new file mode 100644 index 00000000..b2c84488 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt @@ -0,0 +1,14 @@ +package xyz.quaver.pupil.ui.viewmodel + +import androidx.lifecycle.ViewModel +import xyz.quaver.pupil.networking.SearchQuery +import xyz.quaver.pupil.ui.composable.MainRoutes + +class MainViewModel : ViewModel() { + val uiState: MainUIState = MainUIState() +} + +data class MainUIState( + val route: MainRoutes = MainRoutes.SEARCH, + val query: SearchQuery? = null +) \ No newline at end of file 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 index 85c407c8..0bcba433 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt @@ -190,7 +190,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW .header("Referer", "https://hitomi.la/") .build() - client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } + client.newCall(request).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.bytes() } }.getOrNull()?.let { thumbnail -> kotlin.runCatching { cacheFolder.getChild(".thumbnail").also { if (!it.exists()) 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 index ed8f088f..77349b4b 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt @@ -83,10 +83,10 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con @Synchronized fun isDownloading(galleryID: Int): Boolean { - val isThisGallery: (Call) -> Boolean = { !it.isCanceled && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } + 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) } + && client.dispatcher.let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) } } @Synchronized diff --git a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt index 778ff2ee..20947d3c 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt @@ -47,7 +47,7 @@ data class ProxyInfo( Authenticator { _, response -> val credential = Credentials.basic(username, password) - response.request().newBuilder() + response.request.newBuilder() .header("Proxy-Authorization", credential) .build() } diff --git a/app/src/main/java/xyz/quaver/pupil/util/translation.kt b/app/src/main/java/xyz/quaver/pupil/util/translation.kt index 33b7ba05..61b92990 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/translation.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/translation.kt @@ -44,10 +44,10 @@ fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch { translations = emptyMap() kotlin.runCatching { translations = Json.decodeFromString>(client.newCall( - Request.Builder() - .url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json") - .build() - ).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() } + Request.Builder() + .url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json") + .build() + ).execute().also { if (it.code != 200) return@launch }.body?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() } } } @@ -58,7 +58,7 @@ fun getAvailableLanguages(): List { Request.Builder() .url(filesURL) .build() - ).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: return emptyList()) + ).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.string() } ?: return emptyList()) return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull { val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' } diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt index e07a4e16..e8b817aa 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -193,7 +193,7 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: (( override fun onResponse(call: Call, response: Response) { kotlin.runCatching { - val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]") + val data = Json.parseToJsonElement(response.also { if (it.code != 200) throw IOException() }.body.use { it?.string() } ?: "[]") when (data) { is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int }) diff --git a/app/src/main/res/drawable-hdpi/ic_email.png b/app/src/main/res/drawable-hdpi/ic_email.png deleted file mode 100644 index 6cf75e2a23a6e2bdbc6b1df2bb793c005ec79f8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 325 zcmV-L0lNN)P)1ROvpT-zuM;pjaoDMQFGHRiNL(p+Mz7Tpi01kSJ&q2Ro z3V~7Q52PQ_zyXh-X2ZpZauA4bZEej9)Z7o`|Dl3I{(#H^nF+OvC(&EUT;CgB(wy2Paxmq8y)t{t}xgu_Ssz^8cyrAiNu4^P)QhsLG1k?IglEVTAH~+MMZ@jXi*u^!k0k&pGf=? zqz9yzs*V8~8Vof2Aqfsa#}7efP{S>ZK*O4W_#Xw1fnkst%^)+$b__5bEGJ;e51`Mt z1LACL9#G*6e1-r+K?W&Wj$l!91S#}n2xN*EcneRU0vjT-ZxhgG z^&pHRM}2~sr>(8cMud4b_#D(tX?X*5=uwz?W@cuJMCj?p=b(KQI_Mj68UpgmiO{nT zpM!2t;F#|~?1mINYC!&bBJ|zB=b%qyI_3{BM|r`_F)%Pt0E#^zQtu~PIH(q8hN-Ek z1dxB11ihcA9c6DVEiE}INQXD5ny5kb7Ft0`s)zPbJKMJ->CYlVe>bHCBRbv>D=2}X zwTBG-HWXDIRK_m|wz~;cuGGZDRIbD{KX67Fv=>5jJDaHH2aydlWM9@J>8mHUeNA)& zjo2nEB@QCF2}?pFpX4YbyOBQ_yFr7pqc}Lb(t~oaVH6A&002C;3NZ7~JIw$9002ov JPDHLkV1mHy;A#K> diff --git a/app/src/main/res/drawable-hdpi/ic_home.png b/app/src/main/res/drawable-hdpi/ic_home.png deleted file mode 100644 index caccfac02413efafdf8b724b579d244449f9b76a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 279 zcmV+y0qFjTP)wulZ(n2V~#YmULeENm?0u#tv%7eCP9X2FiWEFS^*iR zU0k4p!s~^ZU!Ws}C(ZN%jTD|V-wSj?;Yl;EKK;A%zxs2eI*K1cQx*g+fXNNhP*fLy$mHB!ZoRU=f&{ zfg=Q&J$vTjPLciLZ<|{Ln>}1CrGe1vq?xw?v$36 zwr6|vU*yqohs`zyIt*TSmNz{#_`@-qwcc@(nwr`_1=c4LTvxfDJ>J4_WZSlF&seqI zv48q6koZAKw8Ufjg2Gshn*8N9$&s0u#OS4^Xf}0XGNWfjMbwA-6VV?!mbJ>R j=vbOrXv@XM!@yAX;`-d%Z-cpku4nLc^>bP0l+XkKg>F?d diff --git a/app/src/main/res/drawable-mdpi/ic_help.png b/app/src/main/res/drawable-mdpi/ic_help.png deleted file mode 100644 index b2b8c6361eec33dc23a10089301c0209cc91f4d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 345 zcmV-f0jBQhsZ$ZUEL2Ae{+{nl%5UBY!5dSBDZ-dm5Vm1>{Lnjdi zqhOF8kY0kO1HImb#ehdZ@kmosQwb0Tu|ZOgvl2^Mt2A6r^l3Zbh7iGk#>_#_Zt zgx<#DstBm%9#8`xgV<2tMqrB*tPu}Q5g1`>XlN(}l}8SeM<6kf9F8~%#b&`05+aq5 z1xv75aDzz0e*=TY9~x{0IMm$0X2B;S4F3znQBc=g0@+`2sQW~&1rwl=s{>?zz@zRH zSwRFeSRHD?9s+7_kQK?Hp`pxBwO4dozcj^w{e!RIEo55Msf_kLNAb< r!j32l2o`p9DX!^J;n1hb95Mg^d??g#e>GYd00000NkvXXu0mjfUn7IO diff --git a/app/src/main/res/drawable-mdpi/ic_home.png b/app/src/main/res/drawable-mdpi/ic_home.png deleted file mode 100644 index 1eccf9f3ffdf30ff21d5eb94d6e42541f8c8fb8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjOFdm2Ln>}1CrGe{u=E@|cFb0R zCn@-9%7TS!9QLqu{C76&IO`>Pc;&{%|gt9|CECkT!u>n@n<|5j{|W%6d%W zV6#90h#%w7)BwbcNNO3OV%WfIAXdla3vnPmgr?yHvf*f!oIq216szmeG^(L#s>h?Y z9!;$pG0Bt^3(85d0OUQWHwgw2RQ+KzwV>b?$7Q%WG=d05GJzxwN+=4rT>q7bWLk#B z<;Vb()TMFxqJ;_>trA}x@IbRW3|xd32lxPTaRMr4*a=0nAGF|t6?SIOf(;#j3P3xg y!VXkGI+I(%uEb)&3~JT~jaV$G8ah?xpauY`Fv^{3#c2Hi0000|S0Q0CP!_ETOYKT-PHQuW>9Kv-M*R&?yK4NQJpY^Er+o z_|XQq%N|J6bOHGt#FfxE_O-*lzxXi&fH=l+yb;@B4(}0|*Xn-=fOLioiu_%qy8rM{ zfS+r0K#})~RJX=V0Za`TPk;(QZD9$JEkQ~E)gmN)3D^LT&M@_B*cO0vjZ=OIRRA88 zTLJ2=$QS^ di^cjYy#T|eJ3QV_j_X=c;qL+#ZPO0G>l)slYZJQmO2KaR)FJ5Y7TQ9OTp@9>U-p3{E5n zXE8x;Wx{K<+8P+?DiF@1i?(Ek%sLSFHHiN~A8pF)`2mM<572^eI5Z4n1A))@F8Ghi zGm!EnIKpNIxP&Iz1$0@g6VxSzQ``Z2K@|^m>m0mwsuQR|NPM|m-UR}mXrw97IZa~c z_^3rBbR25Tvtbh?I5)_^2sa{OWjV%#Ba&gMNvajmHY!g{SSNykO-goL85L%mBctQPfxGuKkaA34#hWiy@DNIlwF&vl& ztJtLu*s2^bP4kWkA(NBCrc7@=MGm~>^Z5hD)(sB_YNLDsPl5RNV<=4T@5B2~P9Bfq zfG<1lq32G*HsDKa!}~`8E}!DSRHFD9^z><%-?FSNc>f@}`O)eC4BD%RW%+%a-J8x3 zdTKNpt6^?%=r$&&uWb-R++Q=|&m{1A+NM#MbPUP8Po74lB(jJl#)|}ATia41g7-@T zuO)pkrxCQp9Pe^kBigDqG&(TlK7!=fQtE)CbW=|p#KOr-Xj9L5tfI^T;bRqngDn*f z#5>p$JX)0Hz(Cp|@4r6WO~mnfA}%-*bH$X1OTNThv!*322`JyslO;$|Ox-;I0000< KMNUMnLSTYJO*0<= diff --git a/app/src/main/res/drawable-xhdpi/ic_home.png b/app/src/main/res/drawable-xhdpi/ic_home.png deleted file mode 100644 index cea7b6c519081c4d08b14b4d1c6914d52bbea88f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(Ffw|&IEGZ*dOP(XXS0KV>)C_R z=Q0m0esJ)v*rk@#2=`MSqSq4GGY@RuF4Hu#k>`oRrgwd>wf-Mwb_~DQ-a0>(qe&tC z>`{r&2|s=YJX4t~?4R`O;Q615Pfl;zGb!i0x_-$Yz09AhR^_SOI)8%Owtn)ft(qOz zoAw;IFXDOfda18pTEPTyHoM6!d$xY_b9&5GnfPRLb^PqMis}>GHQR1_?^Cs7{KWKs z@!jMFYN^}g_snd(l(zd|w1h_P7xwds3$|R8-XqGq)c41ON}efZ|C+@O0&cC5w`py> zbgcG+2)ozp4F|h*_Z;n5$iQ_(L*TW&MgtQZ8FDKC9l!jH_|yB1skM3ccz|KT;OXk; Jvd$@?2>|LQiW>j` diff --git a/app/src/main/res/drawable-xhdpi/ic_settings.png b/app/src/main/res/drawable-xhdpi/ic_settings.png deleted file mode 100644 index 92d74d249ea885b9f124689c6599ffca7568956c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 628 zcmV-)0*n2LP) zZ;>SU41bo29z{S1c;(d_;vy)x0mft&a+Mz_uG2;$w#an)0=87of(-E8C6Of&*K&zD z#|x0IcB15vXd`pW;tPA@5VxeWnX5P?nkvMdI>fE~2LW%#65z!Y3h_ni?G)lp!rX&J zeC`qxH;#;I@hz9Pq_T}Z2*kxLyed<8j2|9e9b|1{Y&=v$6ahUCTZy)0E77G0Xlice zu)DnwoHmEhpa=-_7FI_)JtO!YJ# zr!|6rf{E^Qo**D;qJTKTE;REy8Gb$z5U?=$8`(raz}WxH>cfYRKl}pBQNlJ_xKWG% O0000+Ni7zrzj!ZOR{C zybu&pFL>&%WxkJbYQKZ%5}DS;Pq;!{oEn{%EGapBS|Dwa3)cgQ>^<`$kN;O+`)R|I zH+6CUZ|0P_D+5ghgL{I#sB z^x{Ozl_3x0k_=kYU(#T<`+ zdrA1nlgc`m_x`rhE=94+Vzzgcz2v$j84`Et<4pFVw3z9$%5HMpN|Jc=S|IO&UV76# zmGBb^N9tQPY-!&u5>nl--Tq^~z_hd_*^cM5Uud-~6KOn4 diff --git a/app/src/main/res/drawable-xxhdpi/ic_help.png b/app/src/main/res/drawable-xxhdpi/ic_help.png deleted file mode 100644 index 760e70d414bb5bb0528c9cfab54425d6fab66e6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1000 zcmV>P)l; zONBzA3ZI+6P2di25BM1Pm*HdcTgTk9X`Wg7FF{GC)7PPMb?D?Cd=968n3ab{Mjd6& zJuum9_7QYw2Rbw&fMVkj%EmV44wz!GxB?w$c>u-6Eo^fhfVlz6ZV3YOFK?j^L4pJ3 z{sa^pBnZwQ9-uD40Fz3ku7E=C0s!VOMx9W%M8PSS%U3~>-O#|90%;d@Ov*!D10}kl zf-}Wk)K%Pan84|q;klN}#nkvWNBLf%Fzad8D1!)+pAf(Vfs{CBn+VP{NEAyN zcnKzn5%Lk|Bx#()5hddGv9_8J6nb&Nq`v{&hWk#D@jLz&HL0RZf<6=0wHtBa*|&Ks zh)-_9^}~o~5AhzGcO!-XEw+>eWm1Hg=B>?g;QB!f83#5?LfI7ESW+b>pF{B)tCr5K z&j9keMYmO=ZaW;Az%fOMQmJ&`GHmx}kh6cD43td_JI18UB&Ig0VZP0KnF%?roYM$z zMqP%~DZ^_okOikVfUu9wyAQx;fdJI8yub$xvd^4$OUyP)D0!U?%S(L9B){rnL5)BF zvbuTd;&qA}E&w$H0OzwUPz2YXhU-6>+G)BBv3A1Xe1YY_t@CuNR4SJ_9SHW-#e#YX zg3|-bxgDX~d+zpJ06IwsoUaf7-ifg6*B;tDaRF$U060Iuavny2^U71Z!+8K`0u%4! zguLo-9){S*2+Mg!%tv_{Vv>{eIFrfTv*?kdE||Sc(vof-w`3yn&s~A`+o1=FFD*K( z3ANu0dZ_`Pt6k#y$at)RlO8I-itUH%YelJ5-S5{E+m%%s4IRXL60Kn zVHVaoNsmLt3<2~&U5sE-k7UMNyXm3cIH?jn79Kw{seddGceh0UnnFoQNl8gbf_?+m W6z$kIbHm910000+G3&O-?TZd*5O zE^D09DB>Abz!@w|S z^O{w!7F^CQu~7^P+q8I1wa@zS8G@f|;=6mEdHO{yo_{vVFHl5U@Z!D+ldrw9Ev{W_ zbn|8J9Fx83v!~A4r1Ny<>d)*&m(EqCi50!tb?+2&JWyTP#m&L>Ys)re^IK+Cm6?ZH zR^QBRm}By{@2?2+2IIIt3u*)#u60XK$!EGy^|9$wGsBj+ISc+MF|2)VuTjmi;Oif@ zrwt5S=Ivi#!r}1xC#Tn8hAsDAIR0GC&?7&kp5wp{$DhX;dc>zFvn0$}AY;hzmwjn7 z!x!l(eM}eP1FU!)>>XcPFg#{S72!<&ch*6mNuWi6#W!%m^ZQN^wjzfk2O5(F92N|D ZJD6XEh$b&sH@gNT;OXk;vd$@?2>|rnq2mAm diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings.png b/app/src/main/res/drawable-xxhdpi/ic_settings.png deleted file mode 100644 index 2a50df97d209dc681f6187d11bde766f8d823f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 914 zcmV;D18w|?P)6EP8jftpDqgT)Gp6dWAlWOVG1sgQs}baHWVagpF|yObal zGT5Oh9SR~kI7sPG+G1PV+jHn0B;@P9bMAdF@7_QBLC6c|q4l>q1t+!J*` zH-e_|Zuy~)z$1#^=?DKI0?H`H91H@^0Qe*LFZdL^(C*fU&lBJyK^H!v^KD}F$ed1P zj@RG`lYsIK62Ah!0LSXCod&-Z{5l8E)WwR)9IhH%Uoiz|6#Tu-LW+0F6L1N9FZ2l? z*F72QNXBlZMH7IIMfd6xxKAgXwCH|q>4MV>ejfoqpTR>q0BwrssKgBi-wt0Q1n4Jt zj1$m-h@Q$^aN-QrZ!3MkHU{{)dD#j04am zb5g&-IUjlCN({EZP#hLB5!8B(5W9hv}NFeH>?$R)>+$5-$&_p5#w{F<`n zFy=y8ld#x*tgD*FHo}h2#FC78Bz@*^h6lnOhbpdYbkVSmIn0$!wFD#Xv4 zhO;hTok70l-zmh$l4Qlyu<$Shg<`^I4m;XL*I1qtd(xa+jaP&`aJw|j4QE+y@WQZJ z%`)V1!8U1aGDQul-7zow$sEFUX^sQXZAcnsc<20L&gGH=&{@DO*En&_X{8DWppPry)ea=iT{U?x>d&}Hmr+nl6ZIoZ%^}F|6Z<*ogBf#V}qln+8vGN_00}(&SUY}v8e)|<; zju;shG65YY0=n^{EP3nS5dc-as7rBVT(7bXfKCX2c5MJuAOPC70ni!&(2@;+rU-y0 oJsTw4KNfIua&mHVatdDm03M;Q^u1#t>Hq)$07*qoM6N<$f+z@~w*UYD diff --git a/app/src/main/res/drawable/app_icon.xml b/app/src/main/res/drawable/app_icon.xml new file mode 100644 index 00000000..70779250 --- /dev/null +++ b/app/src/main/res/drawable/app_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/arrow_right.xml b/app/src/main/res/drawable/arrow_right.xml deleted file mode 100644 index 0ac62543..00000000 --- a/app/src/main/res/drawable/arrow_right.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/cancel.xml b/app/src/main/res/drawable/cancel.xml deleted file mode 100644 index 147fbdfe..00000000 --- a/app/src/main/res/drawable/cancel.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/clock_end.xml b/app/src/main/res/drawable/clock_end.xml deleted file mode 100644 index 8f636ac6..00000000 --- a/app/src/main/res/drawable/clock_end.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/clock_start.xml b/app/src/main/res/drawable/clock_start.xml deleted file mode 100644 index f131c300..00000000 --- a/app/src/main/res/drawable/clock_start.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/github_circle.xml b/app/src/main/res/drawable/github_circle.xml deleted file mode 100644 index 297143ec..00000000 --- a/app/src/main/res/drawable/github_circle.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_jump.xml b/app/src/main/res/drawable/ic_jump.xml deleted file mode 100644 index 6232799e..00000000 --- a/app/src/main/res/drawable/ic_jump.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_message.xml b/app/src/main/res/drawable/ic_message.xml deleted file mode 100644 index 4e3135eb..00000000 --- a/app/src/main/res/drawable/ic_message.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_progressbar.xml b/app/src/main/res/drawable/ic_progressbar.xml deleted file mode 100644 index 7d0e371f..00000000 --- a/app/src/main/res/drawable/ic_progressbar.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_cache.xml b/app/src/main/res/drawable/ic_progressbar_cache.xml deleted file mode 100644 index ca0643ea..00000000 --- a/app/src/main/res/drawable/ic_progressbar_cache.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_complete.xml b/app/src/main/res/drawable/ic_progressbar_complete.xml deleted file mode 100644 index c23c3b6b..00000000 --- a/app/src/main/res/drawable/ic_progressbar_complete.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_complete_cache.xml b/app/src/main/res/drawable/ic_progressbar_complete_cache.xml deleted file mode 100644 index 2fd4e05c..00000000 --- a/app/src/main/res/drawable/ic_progressbar_complete_cache.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon.xml b/app/src/main/res/drawable/icon.xml deleted file mode 100644 index 8bca8ba9..00000000 --- a/app/src/main/res/drawable/icon.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_red.xml b/app/src/main/res/drawable/icon_red.xml deleted file mode 100644 index 00fdcd21..00000000 --- a/app/src/main/res/drawable/icon_red.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml deleted file mode 100644 index e058761e..00000000 --- a/app/src/main/res/drawable/menu.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/shuffle_variant.xml b/app/src/main/res/drawable/shuffle_variant.xml deleted file mode 100644 index f954eb87..00000000 --- a/app/src/main/res/drawable/shuffle_variant.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/side_nav_bar.png b/app/src/main/res/drawable/side_nav_bar.png deleted file mode 100644 index 727e1e9632ba3a8db52859628e68b39b6e87cf23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68495 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 UXtEsx8G$cN%z5>K->n1w56IyP@&Et; diff --git a/app/src/main/res/drawable/sort_variant.xml b/app/src/main/res/drawable/sort_variant.xml deleted file mode 100644 index 83d5e4cb..00000000 --- a/app/src/main/res/drawable/sort_variant.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_drawable.xml b/app/src/main/res/drawable/thumb_drawable.xml deleted file mode 100644 index f72a3577..00000000 --- a/app/src/main/res/drawable/thumb_drawable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/track.xml b/app/src/main/res/drawable/track.xml deleted file mode 100644 index 770f3075..00000000 --- a/app/src/main/res/drawable/track.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/track_drawable.xml b/app/src/main/res/drawable/track_drawable.xml deleted file mode 100644 index 22113141..00000000 --- a/app/src/main/res/drawable/track_drawable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/gallery_dialog.xml b/app/src/main/res/layout/gallery_dialog.xml deleted file mode 100644 index cb7c017f..00000000 --- a/app/src/main/res/layout/gallery_dialog.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_details.xml b/app/src/main/res/layout/gallery_dialog_details.xml deleted file mode 100644 index 1c3448b7..00000000 --- a/app/src/main/res/layout/gallery_dialog_details.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_dotindicator.xml b/app/src/main/res/layout/gallery_dialog_dotindicator.xml deleted file mode 100644 index 02b91987..00000000 --- a/app/src/main/res/layout/gallery_dialog_dotindicator.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_tags.xml b/app/src/main/res/layout/gallery_dialog_tags.xml deleted file mode 100644 index 539d34a8..00000000 --- a/app/src/main/res/layout/gallery_dialog_tags.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/galleryblock_item.xml b/app/src/main/res/layout/galleryblock_item.xml deleted file mode 100644 index 7638f8e7..00000000 --- a/app/src/main/res/layout/galleryblock_item.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml deleted file mode 100644 index 3a1ebd5f..00000000 --- a/app/src/main/res/layout/main_activity.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity_content.xml b/app/src/main/res/layout/main_activity_content.xml deleted file mode 100644 index abbf1aca..00000000 --- a/app/src/main/res/layout/main_activity_content.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/mirrors_item.xml b/app/src/main/res/layout/mirrors_item.xml deleted file mode 100644 index 2b349771..00000000 --- a/app/src/main/res/layout/mirrors_item.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml deleted file mode 100644 index a413c18b..00000000 --- a/app/src/main/res/layout/nav_header_main.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/swipe_pageturn_view.xml b/app/src/main/res/layout/swipe_pageturn_view.xml deleted file mode 100644 index 6d1f82e2..00000000 --- a/app/src/main/res/layout/swipe_pageturn_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml deleted file mode 100644 index 2d45b181..00000000 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml deleted file mode 100644 index b9fd3fa8..00000000 --- a/app/src/main/res/menu/main.xml +++ /dev/null @@ -1,49 +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 ea44ad2f..26d6fbc1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,9 +21,9 @@ 履歴を削除 履歴を削除しますか? 履歴数: %1$d - 履歴 + 履歴 通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。 - トップ + トップ # リリースノート(v%1$s)\n%2$s セキュリティーモード アプリ履歴でアプリの画面を表示しない @@ -45,7 +45,7 @@ ダウンロード中… ダウンロード完了 バックグラウンドダウンロード中止 - ダウンロード + ダウンロード ページ移動 現ページ番号: %1$d\nページ数: %2$d hitomi.laに接続できません @@ -53,7 +53,7 @@ ダウンロード削除 ダウンロードしたギャラリーを全て削除します。\n実行しますか? ミラーサーバからイメージをロード - ブックマーク + ブックマーク ギャラリー番号で見る エラーが発生しました ストレージ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8554abb8..b242d065 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -20,9 +20,9 @@ 기록 삭제 기록을 삭제하시겠습니까? 기록 %1$d개 저장됨 - 기록 + 기록 백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다. - + # 릴리즈 노트(v%1$s)\n%2$s 최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다 보안 모드 활성화 @@ -44,14 +44,14 @@ 다운로드 중… 다운로드 완료 백그라운드 다운로드 취소 - 다운로드 + 다운로드 페이지 이동 현재 페이지: %1$d\n페이지 수: %2$d hitomi.la에 연결할 수 없습니다 %1$d 페이지로 이동 다운로드 삭제 다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까? - 즐겨찾기 + 즐겨찾기 갤러리 번호로 열기 갤러리를 찾지 못했습니다 저장 공간 diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 99cbc5ee..83638bef 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -1,15 +1,7 @@ - 16dp - 16dp - - 100dp - - 2000px - - 24dp + 24dp 72dp - 300dp \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 83022f36..112999b6 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -3,11 +3,8 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2cc2bbf..9a7720a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,10 +53,10 @@ Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled. - Home - History - Downloads - Favorites + Home + History + Downloads + Favorites Contact Help Visit homepage diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt index 27402b96..e2252de8 100644 --- a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -@file:Suppress("UNUSED_VARIABLE", "IncorrectScope") - package xyz.quaver.pupil /** @@ -26,14 +24,18 @@ package xyz.quaver.pupil * See [testing documentation](http://d.android.com/tools/testing). */ +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Test -import java.lang.reflect.ParameterizedType +import xyz.quaver.pupil.networking.HitomiHttpClient class ExampleUnitTest { @Test - fun test() { - val a = mutableSetOf() + fun test() = runTest { + val hitomi = HitomiHttpClient() - print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType) + val result = hitomi.getGalleryIDsFromNozomi(null, "index", "all") + + println(result.array()) } } diff --git a/build.gradle b/build.gradle index bd179094..f1fcd161 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" diff --git a/gradle.properties b/gradle.properties index f7137ba7..d5361bba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ kotlin.code.style=official android.enableJetifier=true android.useAndroidX=true -kotlin_version=1.9.0 \ No newline at end of file +kotlin_version=1.9.22 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cc366756..7044d5b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip