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