diff --git a/app/build.gradle b/app/build.gradle index d123fc75..b0cf5058 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,27 +14,11 @@ if (file("google-services.json").exists()) { logger.lifecycle("Firebase Disabled") } -ext { - okhttp_version = "3.12.12" -} - -configurations { - all { - resolutionStrategy { - eachDependency { DependencyResolveDetails details -> - if (details.requested.group == "com.squareup.okhttp3" && details.requested.name == "okhttp") { - // OkHttp drops support before 5.0 since 3.13.0 - details.useVersion okhttp_version - } - } - } - } -} - android { + namespace 'xyz.quaver.pupil' defaultConfig { applicationId "xyz.quaver.pupil" - minSdkVersion 16 + minSdkVersion 21 compileSdk 34 targetSdkVersion 34 versionCode 69 @@ -44,8 +28,6 @@ android { } buildTypes { debug { - defaultConfig.minSdkVersion 21 - minifyEnabled false shrinkResources false @@ -65,33 +47,58 @@ android { } buildFeatures { viewBinding true - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" + compose true + buildConfig true } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.9" } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" - implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" + implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0" - implementation "androidx.appcompat:appcompat:1.4.1" - implementation "androidx.activity:activity-ktx:1.4.0" - implementation "androidx.fragment:fragment-ktx:1.4.1" - implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation "androidx.appcompat:appcompat:1.6.1" + implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.fragment:fragment-ktx:1.6.2" + implementation "androidx.preference:preference-ktx:1.2.1" + implementation "androidx.recyclerview:recyclerview:1.3.2" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.biometric:biometric:1.1.0" - implementation "androidx.work:work-runtime-ktx:2.7.1" + implementation "androidx.work:work-runtime-ktx:2.9.0" + + implementation platform("androidx.compose:compose-bom:2024.02.00") + + implementation "androidx.compose.material3:material3" + implementation "androidx.compose.material3:material3-window-size-class" + implementation 'androidx.compose.foundation:foundation' + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation 'androidx.compose.material:material-icons-extended' + implementation 'androidx.activity:activity-compose:1.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' + implementation "com.google.accompanist:accompanist-adaptive:0.34.0" + implementation "androidx.navigation:navigation-compose:2.7.7" + + implementation "androidx.paging:paging-compose:3.2.1" + + implementation "io.ktor:ktor-client-core:2.3.8" + implementation "io.ktor:ktor-client-okhttp:2.3.8" implementation "com.daimajia.swipelayout:library:1.2.0@aar" @@ -112,10 +119,10 @@ dependencies { implementation 'com.github.piasy:BigImageViewer:1.8.1' implementation 'com.github.piasy:FrescoImageLoader:1.8.1' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1' - implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0' + implementation 'com.facebook.fresco:imagepipeline-okhttp3:3.1.3' //noinspection GradleDependency - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2" @@ -133,9 +140,9 @@ dependencies { implementation "xyz.quaver:floatingsearchview:1.1.7" testImplementation "junit:junit:4.13.2" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1" - androidTestImplementation "androidx.test.ext:junit:1.1.3" - androidTestImplementation "androidx.test:rules:1.4.0" - androidTestImplementation "androidx.test:runner:1.4.0" - androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0" + androidTestImplementation "androidx.test.ext:junit:1.1.5" + androidTestImplementation "androidx.test:rules:1.5.0" + androidTestImplementation "androidx.test:runner:1.5.2" + androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index b21d4760..e2755159 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -44,8 +44,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import xyz.quaver.io.FileX -import xyz.quaver.pupil.hitomi.evaluationContext -import xyz.quaver.pupil.hitomi.readText import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.util.* import java.io.File diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt deleted file mode 100644 index 91cec1c5..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2019 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.adapters - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import androidx.vectordrawable.graphics.drawable.Animatable2Compat -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import com.daimajia.swipe.SwipeLayout -import com.daimajia.swipe.adapters.RecyclerSwipeAdapter -import com.daimajia.swipe.interfaces.SwipeAdapterInterface -import com.github.piasy.biv.loader.ImageLoader -import kotlinx.coroutines.* -import xyz.quaver.io.util.getChild -import xyz.quaver.pupil.R -import xyz.quaver.pupil.databinding.GalleryblockItemBinding -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.favorites -import xyz.quaver.pupil.hitomi.getGallery -import xyz.quaver.pupil.hitomi.getGalleryInfo -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.ui.view.ProgressCard -import xyz.quaver.pupil.util.Preferences -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.wordCapitalize -import java.io.File - -class GalleryBlockAdapter(private val galleries: List) : RecyclerSwipeAdapter(), SwipeAdapterInterface { - - var updateAll = true - var thin: Boolean = Preferences["thin"] - - inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) { - private var galleryID: Int = 0 - - init { - CoroutineScope(Dispatchers.Main).launch { - while (updateAll) { - updateProgress(itemView.context) - delay(1000) - } - } - } - - private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch { - with(binding.galleryblockCard) { - val imageList = Cache.getInstance(context, galleryID).metadata.imageList - - if (imageList == null) { - max = 0 - return@with - } - - progress = imageList.count { it != null } - max = imageList.size - - this@GalleryViewHolder.binding.galleryblockId.setOnClickListener { - (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( - ClipData.newPlainText("gallery_id", galleryID.toString()) - ) - Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - - type = if (!imageList.contains(null)) { - val downloadManager = DownloadManager.getInstance(context) - - if (downloadManager.getDownloadFolder(galleryID) == null) - ProgressCard.Type.CACHE - else - ProgressCard.Type.DOWNLOAD - } else - ProgressCard.Type.LOADING - } - } - - fun bind(galleryID: Int) { - this.galleryID = galleryID - updateProgress(itemView.context) - - val cache = Cache.getInstance(itemView.context, galleryID) - - CoroutineScope(Dispatchers.IO).launch { - val galleryBlock = cache.getGalleryBlock() ?: return@launch - - launch(Dispatchers.Main) { - val resources = itemView.context.resources - val languages = resources.getStringArray(R.array.languages).map { - it.split("|").let { split -> - Pair(split[0], split[1]) - } - }.toMap() - - val artists = galleryBlock.artists - val series = galleryBlock.series - - binding.galleryblockThumbnail.apply { - setOnClickListener { - itemView.performClick() - } - setOnLongClickListener { - itemView.performLongClick() - } - setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant)) - setImageLoaderCallback(object: ImageLoader.Callback { - override fun onFail(error: Exception?) { - Cache.delete(context, galleryID) - } - - override fun onCacheHit(imageType: Int, image: File?) {} - override fun onCacheMiss(imageType: Int, image: File?) {} - override fun onFinish() {} - override fun onProgress(progress: Int) {} - override fun onStart() {} - override fun onSuccess(image: File?) {} - }) - ssiv?.recycle() - CoroutineScope(Dispatchers.IO).launch { - cache.getThumbnail().let { launch(Dispatchers.Main) { - showImage(it) - } } - } - } - - binding.galleryblockTitle.text = galleryBlock.title - with(binding.galleryblockArtist) { - text = artists.joinToString { it.wordCapitalize() } - visibility = when { - artists.isNotEmpty() -> View.VISIBLE - else -> View.GONE - } - - CoroutineScope(Dispatchers.IO).launch { - val gallery = runCatching { - getGallery(galleryID) - }.getOrNull() - - if (gallery?.groups?.isNotEmpty() != true) - return@launch - - launch(Dispatchers.Main) { - text = context.getString( - R.string.galleryblock_artist_with_group, - artists.joinToString { it.wordCapitalize() }, - gallery.groups.joinToString { it.wordCapitalize() } - ) - } - } - } - with(binding.galleryblockSeries) { - text = - resources.getString( - R.string.galleryblock_series, - series.joinToString(", ") { it.wordCapitalize() }) - visibility = when { - series.isNotEmpty() -> View.VISIBLE - else -> View.GONE - } - } - binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize() - with(binding.galleryblockLanguage) { - text = - resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) - visibility = when { - !galleryBlock.language.isNullOrEmpty() -> View.VISIBLE - else -> View.GONE - } - } - - with(binding.galleryblockTagGroup) { - onClickListener = { - onChipClickedHandler.forEach { callback -> - callback.invoke(it) - } - } - - tags.clear() - - CoroutineScope(Dispatchers.IO).launch { - tags.addAll( - galleryBlock.relatedTags.sortedBy { - val tag = Tag.parse(it) - - if (favoriteTags.contains(tag)) - -1 - else - when(Tag.parse(it).area) { - "female" -> 0 - "male" -> 1 - else -> 2 - } - }.map { - Tag.parse(it) - } - ) - - launch(Dispatchers.Main) { - refresh() - } - } - } - - binding.galleryblockId.text = galleryBlock.id.toString() - binding.galleryblockPagecount.text = "-" - CoroutineScope(Dispatchers.IO).launch { - val pageCount = kotlin.runCatching { - getGalleryInfo(galleryBlock.id).files.size - }.getOrNull() ?: return@launch - withContext(Dispatchers.Main) { - binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount) - } - } - - with(binding.galleryblockFavorite) { - setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty) - setOnClickListener { - when { - favorites.contains(galleryBlock.id) -> { - favorites.remove(galleryBlock.id) - - setImageResource(R.drawable.ic_star_empty) - } - else -> { - favorites.add(galleryBlock.id) - - setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply { - this ?: return@apply - - registerAnimationCallback(object: Animatable2Compat.AnimationCallback() { - override fun onAnimationEnd(drawable: Drawable?) { - setImageResource(R.drawable.ic_star_filled) - } - }) - start() - }) - } - } - } - } - - - } - } - - // Make some views invisible to make it thinner - if (thin) { - binding.galleryblockTagGroup.visibility = View.GONE - } - } - } - - val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() - var onDownloadClickedHandler: ((Int) -> Unit)? = null - var onDeleteClickedHandler: ((Int) -> Unit)? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is GalleryViewHolder) { - val galleryID = galleries[position] - - holder.bind(galleryID) - - holder.binding.galleryblockCard.binding.download.setOnClickListener { - onDownloadClickedHandler?.invoke(position) - } - - holder.binding.galleryblockCard.binding.delete.setOnClickListener { - onDeleteClickedHandler?.invoke(position) - } - - mItemManger.bindView(holder.binding.root, position) - - holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener { - override fun onStartOpen(layout: SwipeLayout?) { - mItemManger.closeAllExcept(layout) - - holder.binding.galleryblockCard.binding.download.text = - if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID)) - holder.binding.root.context.getString(android.R.string.cancel) - else - holder.binding.root.context.getString(R.string.main_download) - } - - override fun onClose(layout: SwipeLayout?) {} - override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {} - override fun onOpen(layout: SwipeLayout?) {} - override fun onStartClose(layout: SwipeLayout?) {} - override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {} - }) - } - } - - override fun getItemCount() = galleries.size - - override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt deleted file mode 100644 index 3b9a081b..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2019 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.adapters - -import android.net.Uri -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import com.github.piasy.biv.view.BigImageView -import xyz.quaver.pupil.R - -class ThumbnailAdapter(var thumbnails: List) : RecyclerView.Adapter() { - - class ViewHolder(val view: BigImageView) : RecyclerView.ViewHolder(view) { - fun clear() { - view.ssiv?.recycle() - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(BigImageView(parent.context).apply { - setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant)) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.view.showImage(Uri.parse(thumbnails[position])) - } - - override fun getItemCount() = thumbnails.size - - override fun onViewRecycled(holder: ViewHolder) { - holder.clear() - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt deleted file mode 100644 index 22667059..00000000 --- a/app/src/main/java/xyz/quaver/pupil/adapters/ThumbnailPageAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.adapters - -import android.view.ViewGroup -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlin.math.min - -class ThumbnailPageAdapter(private val thumbnails: List) : RecyclerView.Adapter() { - - class ViewHolder(val view: RecyclerView) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(RecyclerView(parent.context).apply { - val layoutManager = GridLayoutManager(parent.context, 3) - val adapter = ThumbnailAdapter(listOf()) - - this.layoutManager = layoutManager - this.adapter = adapter - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - }) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - (holder.view.adapter as ThumbnailAdapter).apply { - thumbnails = this@ThumbnailPageAdapter.thumbnails.slice(9*position until min(9*position+9, this@ThumbnailPageAdapter.thumbnails.size)) - notifyDataSetChanged() - - (holder.view.layoutManager as GridLayoutManager).scrollToPosition(8) - } - } - - override fun getItemCount() = if (thumbnails.isEmpty()) 0 else thumbnails.size/9 + if (thumbnails.size%9 != 0) 1 else 0 - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt index abea6418..13d92e0e 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt @@ -110,7 +110,7 @@ fun URL.readText(settings: HeaderSetter? = null): String { settings?.invoke(it) ?: it }.build() - return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.string() } ?: throw IOException() + return client.newCall(request).execute().also{ if (it.code != 200) throw IOException("CODE ${it.code}") }.body?.use { it.string() } ?: throw IOException() } fun URL.readBytes(settings: HeaderSetter? = null): ByteArray { @@ -119,7 +119,7 @@ fun URL.readBytes(settings: HeaderSetter? = null): ByteArray { settings?.invoke(it) ?: it }.build() - return client.newCall(request).execute().also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw IOException() + return client.newCall(request).execute().also { if (it.code != 200) throw IOException("CODE ${it.code}") }.body?.use { it.bytes() } ?: throw IOException() } @Suppress("EXPERIMENTAL_API_USAGE") @@ -161,8 +161,8 @@ object gg { } override fun onResponse(call: Call, response: Response) { - if (!call.isCanceled) { - response.body()?.use { + if (!call.isCanceled()) { + response.body?.use { continuation.resume(it.string()) { call.cancel() } diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt index b4eaecf6..ef622bce 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt @@ -50,7 +50,7 @@ fun sanitize(input: String) : String { } fun getIndexVersion(name: String) = - URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText() + URL("$protocol//${xyz.quaver.pupil.networking.domain}/$name/version?_=${System.currentTimeMillis()}").readText() //search.js fun getGalleryIDsForQuery(query: String) : Set { @@ -115,7 +115,7 @@ fun getSuggestionsForQuery(query: String) : List { data class Suggestion(val s: String, val t: Int, val u: String, val n: String) fun getSuggestionsFromData(field: String, data: Pair) : List { - val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data" + val url = "$protocol//${xyz.quaver.pupil.networking.domain}/$index_dir/$field.$tag_index_version.data" val (offset, length) = data if (length > 10000 || length <= 0) throw Exception("length $length is too long") @@ -162,8 +162,8 @@ fun getSuggestionsFromData(field: String, data: Pair) : List { val nozomiAddress = when(area) { - null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension" - else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" + null -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$tag-$language$nozomiextension" + else -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension" } val bytes = try { @@ -185,7 +185,7 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set< } fun getGalleryIDsFromData(data: Pair) : Set { - val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data" + val url = "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/galleries.$galleries_index_version.data" val (offset, length) = data if (length > 100000000 || length <= 0) throw Exception("length $length is too long") @@ -216,10 +216,10 @@ fun getGalleryIDsFromData(data: Pair) : Set { fun getNodeAtAddress(field: String, address: Long) : Node? { val url = when(field) { - "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index" - "languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index" - "nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index" - else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index" + "galleries" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/galleries.$galleries_index_version.index" + "languages" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/languages.$galleries_index_version.index" + "nozomiurl" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/nozomiurl.$galleries_index_version.index" + else -> "$protocol//${xyz.quaver.pupil.networking.domain}/$index_dir/$field.$tag_index_version.index" } val nodedata = getURLAtRange(url, address.until(address+ max_node_size)) @@ -233,7 +233,7 @@ fun getURLAtRange(url: String, range: LongRange) : ByteArray { .header("Range", "bytes=${range.first}-${range.last}") .build() - return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf() + return client.newCall(request).execute().body?.use { it.bytes() } ?: byteArrayOf() } @OptIn(ExperimentalUnsignedTypes::class) diff --git a/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt new file mode 100644 index 00000000..6ec8cd21 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/HitomiHttpClient.kt @@ -0,0 +1,208 @@ +package xyz.quaver.pupil.networking + +import androidx.collection.mutableIntSetOf +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import xyz.quaver.pupil.hitomi.max_node_size +import java.nio.ByteBuffer +import java.nio.IntBuffer + +const val domain = "ltn.hitomi.la" +const val galleryBlockExtension = ".html" +const val galleryBlockDir = "galleryblock" +const val nozomiExtension = ".nozomi" + +const val compressedNozomiPrefix = "n" + +const val B = 16 +const val indexDir = "tagindex" +const val galleriesIndexDir = "galleriesindex" +const val languagesIndexDir = "languagesindex" +const val nozomiURLIndexDir = "nozomiurlindex" + +fun IntBuffer.toSet(): Set { + val result = mutableSetOf() + + while (this.hasRemaining()) { + result.add(this.get()) + } + + return result +} + +class HitomiHttpClient { + private val httpClient = HttpClient(OkHttp) + + private var _tagIndexVersion: String? = null + private suspend fun getTagIndexVersion(): String = + _tagIndexVersion ?: getIndexVersion("tagindex").also { + _tagIndexVersion = it + } + + private var _galleriesIndexVersion: String? = null + private suspend fun getGalleriesIndexVersion(): String = + _galleriesIndexVersion ?: getIndexVersion("galleriesindex").also { + _galleriesIndexVersion = it + } + + private suspend fun getIndexVersion(name: String): String = withContext(Dispatchers.IO) { + httpClient.get("https://$domain/$name/version?_=${System.currentTimeMillis()}").bodyAsText() + } + + private suspend fun getURLAtRange(url: String, range: LongRange): ByteBuffer { + val response: HttpResponse = withContext(Dispatchers.IO) { + httpClient.get(url) { + header("Range", "bytes=${range.first}-${range.last}") + } + } + + val result: ByteArray = response.body() + + return ByteBuffer.wrap(result) + } + + private suspend fun getNodeAtAddress(field: String, address: Long): Node { + val url = when (field) { + "galleries" -> "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.index" + "languages" -> "https://$domain/$galleriesIndexDir/languages.${getGalleriesIndexVersion()}.index" + "nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${getGalleriesIndexVersion()}.index" + else -> "https://$domain/$indexDir/$field.${getTagIndexVersion()}.index" + } + + return Node.decodeNode( + getURLAtRange(url, address until (address+max_node_size)) + ) + } + + private suspend fun bSearch( + field: String, + key: Node.Key, + node: Node + ): Node.Data? { + if (node.keys.isEmpty()) { + return null + } + + val (matched, index) = node.locateKey(key) + + if (matched) { + return node.datas[index] + } else if (node.isLeaf) { + return null + } + + val nextNode = getNodeAtAddress(field, node.subNodeAddresses[index]) + return bSearch(field, key, nextNode) + } + + private suspend fun getGalleryIDsFromData(offset: Long, length: Int): IntBuffer { + val url = "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.data" + if (length > 100000000 || length <= 0) { + error("length $length is too long") + } + + return getURLAtRange(url, offset until (offset+length)).asIntBuffer() + } + + suspend fun getGalleryIDsFromNozomi( + area: String?, + tag: String, + language: String + ): IntBuffer { + val nozomiAddress = if (area == null) { + "https://$domain/$compressedNozomiPrefix/$tag-$language$nozomiExtension" + } else { + "https://$domain/$compressedNozomiPrefix/$area/$tag-$language$nozomiExtension" + } + + val response: HttpResponse = withContext(Dispatchers.IO) { + httpClient.get(nozomiAddress) + } + + val result: ByteArray = response.body() + + return ByteBuffer.wrap(result).asIntBuffer() + } + + suspend fun getGalleryIDsForQuery(query: SearchQuery.Tag, language: String = "all"): IntBuffer = when (query.namespace) { + "female", "male" -> getGalleryIDsFromNozomi("tag", query.toString(), language) + "language" -> getGalleryIDsFromNozomi(null, "index", query.tag) + null -> { + val key = Node.Key(query.tag) + + val node = getNodeAtAddress("galleries", 0) + val data = bSearch("galleries", key, node) + + if (data != null) getGalleryIDsFromData(data.offset, data.length) else IntBuffer.allocate(0) + } + else -> getGalleryIDsFromNozomi(query.namespace, query.tag, language) + } + + suspend fun search(query: SearchQuery?): Set = when (query) { + is SearchQuery.Tag -> getGalleryIDsForQuery(query).toSet() + is SearchQuery.Not -> coroutineScope { + val allGalleries = async { + getGalleryIDsFromNozomi(null, "index", "all") + } + + val queriedGalleries = search(query.query) + + val result = mutableSetOf() + + with (allGalleries.await()) { + while (this.hasRemaining()) { + val gallery = this.get() + + if (gallery in queriedGalleries) { + result.add(gallery) + } + } + } + + result + } + is SearchQuery.And -> coroutineScope { + val queries = query.queries.map { query -> + async { + search(query) + } + } + + val result = queries.first().await().toMutableSet() + + queries.drop(1).forEach { + val queryResult = it.await() + + result.retainAll(queryResult) + } + + result + } + is SearchQuery.Or -> coroutineScope { + val queries = query.queries.map { query -> + async { + search(query) + } + } + + val result = mutableSetOf() + + queries.forEach { + val queryResult = it.await() + result.addAll(queryResult) + } + + result + } + null -> getGalleryIDsFromNozomi(null, "index", "all").toSet() + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/networking/Node.kt b/app/src/main/java/xyz/quaver/pupil/networking/Node.kt new file mode 100644 index 00000000..6e90284b --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/Node.kt @@ -0,0 +1,107 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package xyz.quaver.pupil.networking + +import java.nio.ByteBuffer +import java.security.MessageDigest +import kotlin.math.min + +private fun sha256(data: ByteArray): ByteArray = + MessageDigest.getInstance("SHA-256").digest(data) + +private fun hashTerm(term: String): UByteArray = + sha256(term.toByteArray()).sliceArray(0..<4).toUByteArray() + +data class Node( + val keys: List, + val datas: List, + val subNodeAddresses: List +) { + data class Key( + private val key: UByteArray + ): Comparable { + + constructor(term: String): this(hashTerm(term)) + + override fun compareTo(other: Key): Int { + val minSize = min(this.key.size, other.key.size) + + for (i in 0.. other.key[i]) { + return 1 + } + } + + return 0 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Key + + return key.contentEquals(other.key) + } + + override fun hashCode(): Int { + return key.contentHashCode() + } + } + + data class Data( + val offset: Long, + val length: Int + ) + + companion object { + fun decodeNode(buffer: ByteBuffer): Node { + val numberOfKeys = buffer.int + val keys = mutableListOf() + + for (i in 0..() + + for (i in 0..() + + for (i in 0.. { + val index = keys.indexOfFirst { key -> key <= target } + + if (index == -1) { + return Pair(false, keys.size) + } + + return Pair(keys[index] == target, index) + } +} + diff --git a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt new file mode 100644 index 00000000..f242d6e1 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt @@ -0,0 +1,48 @@ +package xyz.quaver.pupil.networking + +sealed interface SearchQuery { + data class Tag( + val namespace: String?, + val tag: String + ): SearchQuery { + companion object { + fun parseTag(tag: String): Tag { + val splitTag = tag.split(':', limit = 1) + + return if (splitTag.size == 1) { + Tag(null, tag) + } else { + Tag(splitTag[0], splitTag[1]) + } + } + } + + override fun toString() = if (namespace == null) tag else "$namespace:$tag" + } + + + data class And( + val queries: List + ): SearchQuery { + init { + if (queries.isEmpty()) { + error("queries cannot be empty") + } + } + } + + data class Or( + val queries: List + ): SearchQuery { + init { + if (queries.isEmpty()) { + error("queries cannot be empty") + } + } + } + + data class Not( + val query: SearchQuery + ): SearchQuery + +} diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt index 54bf595e..77f27cc6 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt @@ -149,7 +149,7 @@ class DownloadService : Service() { override fun source(): BufferedSource { if (bufferedSource == null) - bufferedSource = Okio.buffer(source(responseBody.source())) + bufferedSource = source(responseBody.source()).buffer() return bufferedSource!! } @@ -177,7 +177,7 @@ class DownloadService : Service() { var limit = 10 while (response?.isSuccessful != true) { - if (response?.code() == 503) { + if (response?.code == 503) { Thread.sleep(200) } else if (--limit < 0) break @@ -191,7 +191,7 @@ class DownloadService : Service() { response = chain.proceed(request) response!!.newBuilder() - .body(response.body()?.let { + .body(response.body?.let { ProgressResponseBody(request.tag(), it, progressListener) }).build() } @@ -228,11 +228,11 @@ class DownloadService : Service() { override fun onResponse(call: Call, response: Response) { Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}") val (galleryID, index, startId) = call.request().tag() as Tag - val ext = call.request().url().encodedPath().split('.').last() + val ext = call.request().url.encodedPath.split('.').last() CoroutineScope(Dispatchers.IO).launch { runCatching { - val image = response.also { if (it.code() != 200) throw IOException( "$galleryID $index ${response.request().url()} CODE ${it.code()}" ) }.body()?.use { it.bytes() } ?: throw Exception("Response null") + val image = response.also { if (it.code != 200) throw IOException( "$galleryID $index ${response.request.url} CODE ${it.code}" ) }.body?.use { it.bytes() } ?: throw Exception("Response null") val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt() Cache.getInstance(this@DownloadService, galleryID) @@ -257,13 +257,13 @@ class DownloadService : Service() { } fun cancel(startId: Int? = null) { - client.dispatcher().queuedCalls().filter { + client.dispatcher.queuedCalls().filter { it.request().tag() is Tag }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } it.cancel() } - client.dispatcher().runningCalls().filter { + client.dispatcher.runningCalls().filter { it.request().tag() is Tag }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } @@ -278,13 +278,13 @@ class DownloadService : Service() { } fun cancel(galleryID: Int, startId: Int? = null) { - client.dispatcher().queuedCalls().filter { + client.dispatcher.queuedCalls().filter { (it.request().tag() as? Tag)?.galleryID == galleryID }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } it.cancel() } - client.dispatcher().runningCalls().filter { + client.dispatcher.runningCalls().filter { (it.request().tag() as? Tag)?.galleryID == galleryID }.forEach { (it.request().tag() as? Tag)?.startId?.let { stopSelf(it) } @@ -350,7 +350,7 @@ class DownloadService : Service() { val queued = mutableSetOf() if (priority) { - client.dispatcher().queuedCalls().forEach { + client.dispatcher.queuedCalls().forEach { val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach if (queued.add(queuedID)) diff --git a/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt b/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt deleted file mode 100644 index 86cef085..00000000 --- a/app/src/main/java/xyz/quaver/pupil/types/Suggestions.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2019 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.types - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.pupil.hitomi.Suggestion -import xyz.quaver.pupil.util.translations - -@Parcelize -data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { - constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) - - @IgnoredOnParcel - override val body = - if (translations[s] != null) - "${translations[s]} ($s)" - else - s -} - -@Parcelize -class Suggestion(override val body: String) : SearchSuggestion - -@Parcelize -class NoResultSuggestion(override val body: String) : SearchSuggestion - -@Parcelize -class LoadingSuggestion(override val body: String) : SearchSuggestion - -@Parcelize -@Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY") -class FavoriteHistorySwitch(override val body: String) : SearchSuggestion \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index e862fcc0..ff619f34 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -18,842 +18,36 @@ package xyz.quaver.pupil.ui -import android.Manifest -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build import android.os.Bundle -import android.text.InputType -import android.text.util.Linkify -import android.view.KeyEvent -import android.view.MenuItem -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.EditText -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.view.GravityCompat -import androidx.core.view.ViewCompat -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.navigation.NavigationView -import com.google.android.material.snackbar.Snackbar -import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.* -import xyz.quaver.floatingsearchview.FloatingSearchView -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.floatingsearchview.util.view.MenuView -import xyz.quaver.floatingsearchview.util.view.SearchInputView -import xyz.quaver.pupil.* -import xyz.quaver.pupil.adapters.GalleryBlockAdapter -import xyz.quaver.pupil.databinding.MainActivityBinding -import xyz.quaver.pupil.hitomi.doSearch -import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi -import xyz.quaver.pupil.hitomi.getSuggestionsForQuery -import xyz.quaver.pupil.services.DownloadService -import xyz.quaver.pupil.types.* -import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment -import xyz.quaver.pupil.ui.dialog.GalleryDialog -import xyz.quaver.pupil.ui.view.MainView -import xyz.quaver.pupil.ui.view.ProgressCard -import xyz.quaver.pupil.util.ItemClickSupport -import xyz.quaver.pupil.util.Preferences -import xyz.quaver.pupil.util.requestNotificationPermission -import xyz.quaver.pupil.util.checkUpdate -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.restore -import xyz.quaver.pupil.util.showNotificationPermissionExplanationDialog -import java.util.regex.Pattern -import kotlin.math.ceil -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -class MainActivity : - BaseActivity(), - NavigationView.OnNavigationItemSelectedListener -{ - - enum class Mode { - SEARCH, - HISTORY, - DOWNLOAD, - FAVORITE - } - - enum class SortMode { - NEWEST, - POPULAR - } - - private val galleries = ArrayList() - - private var query = "" - set(value) { - field = value - with(findViewById(R.id.search_bar_text)) { - if (text.toString() != value) - setText(query, TextView.BufferType.EDITABLE) - } - } - private var queryStack = mutableListOf() - - private var mode = Mode.SEARCH - private var sortMode = SortMode.NEWEST - - private var galleryIDs: Deferred>? = null - private var totalItems = 0 - private var loadingJob: Job? = null - private var currentPage = 0 - - private lateinit var binding: MainActivityBinding - - private val requestNotificationPermssionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (!isGranted) { - showNotificationPermissionExplanationDialog(this) - } - } +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import com.google.accompanist.adaptive.calculateDisplayFeatures +import xyz.quaver.pupil.ui.composable.PupilApp +import xyz.quaver.pupil.ui.theme.AppTheme +import xyz.quaver.pupil.ui.viewmodel.MainViewModel +class MainActivity : BaseActivity() { + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) - binding = MainActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - if (intent.action == Intent.ACTION_VIEW) { - intent.dataString?.let { url -> - restore(url, - onFailure = { - Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() - }, onSuccess = { - Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show() - } + val viewModel: MainViewModel by viewModels() + + setContent { + AppTheme { + val windowSize = calculateWindowSizeClass(this) + val displayFeatures = calculateDisplayFeatures(this) + + PupilApp( + windowSize = windowSize, + displayFeatures = displayFeatures, + uiState = viewModel.uiState ) } } - - requestNotificationPermission(this, requestNotificationPermssionLauncher, false) {} - - if (Preferences["download_folder", ""].isEmpty()) - DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] && - ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() } - .contains(Preferences["download_folder", ""]) - ) { - AlertDialog.Builder(this) - .setTitle(R.string.warning) - .setMessage(R.string.unaccessible_download_folder) - .setPositiveButton(android.R.string.ok) { _, _ -> - DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") - }.setNegativeButton(R.string.ignore) { _, _ -> - Preferences["download_folder_ignore_warning"] = true - }.show() - } - - initView() - } - - override fun onResume() { - super.onResume() - - checkUpdate(this) - } - - @OptIn(ExperimentalStdlibApi::class) - override fun onBackPressed() { - when { - binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START) - queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { - query = queryStack.last() - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - else -> super.onBackPressed() - } - } - - override fun onDestroy() { - super.onDestroy() - - (binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - val perPage = Preferences["per_page", "25"].toInt() - val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt() - - return when(keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> { - if (currentPage > 0) { - runOnUiThread { - currentPage-- - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - - true - } - KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (currentPage < maxPage) { - runOnUiThread { - currentPage++ - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - - true - } - else -> super.onKeyDown(keyCode, event) - } - } - - private fun initView() { - binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - // -height of the search view < translationY < 0 - binding.contents.searchview.translationY = - min( - max( - binding.contents.searchview.translationY - dy, - -binding.contents.searchview.binding.querySection.root.height.toFloat() - ), 0F) - - if (dy > 0) - binding.contents.fab.hideMenuButton(true) - else if (dy < 0) - binding.contents.fab.showMenuButton(true) - } - }) - - Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) }) - - //NavigationView - binding.navView.setNavigationItemSelectedListener(this) - - with(binding.contents.cancelFab) { - setImageResource(R.drawable.cancel) - setOnClickListener { - DownloadService.cancel(this@MainActivity) - } - } - - with(binding.contents.jumpFab) { - setImageResource(R.drawable.ic_jump) - setOnClickListener { - val perPage = Preferences["per_page", "25"].toInt() - val editText = EditText(context) - - AlertDialog.Builder(context).apply { - setView(editText) - setTitle(R.string.main_jump_title) - setMessage(getString( - R.string.main_jump_message, - currentPage+1, - ceil(totalItems / perPage.toDouble()).roundToInt() - )) - - setPositiveButton(android.R.string.ok) { _, _ -> - currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 - - runOnUiThread { - cancelFetch() - clearGalleries() - loadBlocks() - } - } - }.show() - } - } - - with(binding.contents.randomFab) { - setImageResource(R.drawable.shuffle_variant) - setOnClickListener { - runBlocking { - withTimeoutOrNull(100) { - galleryIDs?.await() - } - }.let { - if (it?.isEmpty() == false) { - val galleryID = it.random() - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - } - } - } - } - - with(binding.contents.idFab) { - setImageResource(R.drawable.numeric) - setOnClickListener { - val editText = EditText(context).apply { - inputType = InputType.TYPE_CLASS_NUMBER - } - - AlertDialog.Builder(context).apply { - setView(editText) - setTitle(R.string.main_open_gallery_by_id) - - setPositiveButton(android.R.string.ok) { _, _ -> - val galleryID = editText.text.toString().toIntOrNull() ?: return@setPositiveButton - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - } - }.show() - } - } - - with(binding.contents.view) { - setOnPageTurnListener(object: MainView.OnPageTurnListener { - override fun onPrev(page: Int) { - currentPage-- - - // disable pageturn until the contents are loaded - setCurrentPage(1, false) - - ViewCompat.animate(binding.contents.searchview) - .setDuration(100) - .setInterpolator(DecelerateInterpolator()) - .translationY(0F) - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - - override fun onNext(page: Int) { - currentPage++ - - // disable pageturn until the contents are loaded - setCurrentPage(1, false) - - ViewCompat.animate(binding.contents.searchview) - .setDuration(100) - .setInterpolator(DecelerateInterpolator()) - .translationY(0F) - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - }) - } - - setupSearchBar() - setupRecyclerView() - fetchGalleries(query, sortMode) - loadBlocks() - } - - @SuppressLint("ClickableViewAccessibility") - private fun setupRecyclerView() { - with(binding.contents.recyclerview) { - adapter = GalleryBlockAdapter(galleries).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - onDownloadClickedHandler = { position -> - val galleryID = galleries[position] - - requestNotificationPermission( - this@MainActivity, - requestNotificationPermssionLauncher - ) { - if (DownloadManager.getInstance(context).isDownloading(galleryID)) { //download in progress - DownloadService.cancel(this@MainActivity, galleryID) - } - else { - DownloadManager.getInstance(context).addDownloadFolder(galleryID) - DownloadService.download(this@MainActivity, galleryID) - } - } - - closeAllItems() - } - - onDeleteClickedHandler = { position -> - val galleryID = galleries[position] - DownloadService.delete(this@MainActivity, galleryID) - - histories.remove(galleryID) - - if (this@MainActivity.mode != Mode.SEARCH) - runOnUiThread { - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - - closeAllItems() - } - } - ItemClickSupport.addTo(this).apply { - onItemClickListener = listener@{ _, position, v -> - if (v !is ProgressCard) - return@listener - - val intent = Intent(this@MainActivity, ReaderActivity::class.java) - intent.putExtra("galleryID", galleries[position]) - - //TODO: Maybe sprinkling some transitions will be nice :D - startActivity(intent) - } - - onItemLongClickListener = listener@{ _, position, v -> - if (v !is ProgressCard) - return@listener false - - val galleryID = galleries.getOrNull(position) ?: return@listener true - - GalleryDialog(this@MainActivity, galleryID).apply { - onChipClickedHandler.add { - runOnUiThread { - query = it.toQuery() - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - dismiss() - } - }.show() - - true - } - } - } - } - - private var isFavorite = false - private val defaultSuggestions: List - get() = when { - isFavorite -> { - favoriteTags.map { - TagSuggestion(it.tag, -1, "", it.area ?: "tag") - } + FavoriteHistorySwitch(getString(R.string.search_show_histories)) - } - else -> { - searchHistory.map { - Suggestion(it) - }.takeLast(10) + FavoriteHistorySwitch(getString(R.string.search_show_tags)) - } - }.reversed() - - private var suggestionJob : Job? = null - private fun setupSearchBar() { - with(binding.contents.searchview) { - onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { - override fun onMenuOpened() { - (this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems() - } - - override fun onMenuClosed() { - //Do Nothing - } - } - - post { - findViewById(R.id.menu_view).menuItems.firstOrNull { - (it as MenuItem).itemId == R.id.main_menu_thin - }?.let { - (it as MenuItem).isChecked = Preferences["thin"] - } - } - - onHistoryDeleteClickedListener = { - searchHistory.remove(it) - swapSuggestions(defaultSuggestions) - } - onFavoriteHistorySwitchClickListener = { - isFavorite = !isFavorite - swapSuggestions(defaultSuggestions) - } - - onMenuItemClickListener = { - onActionMenuItemSelected(it) - } - - onQueryChangeListener = lambda@{ _, query -> - this@MainActivity.query = query - - suggestionJob?.cancel() - - if (query.isEmpty() or query.endsWith(' ')) { - swapSuggestions(defaultSuggestions) - - return@lambda - } - - swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString()))) - - val currentQuery = query.split(" ").last() - .replace(Regex("^-"), "") - .replace('_', ' ') - - suggestionJob = CoroutineScope(Dispatchers.IO).launch { - val suggestions = kotlin.runCatching { - getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }.toMutableList() - }.getOrElse { mutableListOf() } - - suggestions.filter { - val tag = "${it.n}:${it.s.replace(Regex("\\s"), "_")}" - favoriteTags.contains(Tag.parse(tag)) - }.reversed().forEach { - suggestions.remove(it) - suggestions.add(0, it) - } - - withContext(Dispatchers.Main) { - swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString()))) - } - } - } - - onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener { - override fun onFocus() { - if (query.isEmpty() or query.endsWith(' ')) - swapSuggestions(defaultSuggestions) - } - - override fun onFocusCleared() { - suggestionJob?.cancel() - - runOnUiThread { - cancelFetch() - clearGalleries() - currentPage = 0 - fetchGalleries(query, sortMode) - loadBlocks() - } - } - } - - attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer) - } - } - - fun onActionMenuItemSelected(item: MenuItem?) { - when(item?.itemId) { - R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) - R.id.main_menu_thin -> { - val thin = !item.isChecked - - item.isChecked = thin - binding.contents.recyclerview.apply { - (adapter as GalleryBlockAdapter).apply { - this.thin = thin - - Preferences["thin"] = thin - } - - adapter = adapter // Force to redraw - } - } - R.id.main_menu_sort_newest -> { - sortMode = SortMode.NEWEST - item.isChecked = true - - runOnUiThread { - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - R.id.main_menu_sort_popular -> { - sortMode = SortMode.POPULAR - item.isChecked = true - - runOnUiThread { - currentPage = 0 - - cancelFetch() - clearGalleries() - fetchGalleries(query, sortMode) - loadBlocks() - } - } - } - } - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - runOnUiThread { - binding.drawer.closeDrawers() - - when(item.itemId) { - R.id.main_drawer_home -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.SEARCH - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_history -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.HISTORY - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_downloads -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.DOWNLOAD - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_favorite -> { - cancelFetch() - clearGalleries() - currentPage = 0 - query = "" - queryStack.clear() - mode = Mode.FAVORITE - fetchGalleries(query, sortMode) - loadBlocks() - } - R.id.main_drawer_help -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) - } - R.id.main_drawer_github -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github)))) - } - R.id.main_drawer_homepage -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.home_page)))) - } - R.id.main_drawer_email -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) - } - R.id.main_drawer_kakaotalk -> { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord)))) - } - } - } - - return true - } - - private fun cancelFetch() { - galleryIDs?.cancel() - loadingJob?.cancel() - } - - private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch { - galleries.clear() - - with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) { - this ?: return@with - - this.notifyDataSetChanged() - } - - binding.contents.noresult.visibility = View.INVISIBLE - binding.contents.progressbar.show() - } - - private fun fetchGalleries(query: String, sortMode: SortMode) { - val defaultQuery: String = Preferences["default_query"] - - if (query.isNotBlank()) - searchHistory.add(query) - - if (query != queryStack.lastOrNull()) { - queryStack.remove(query) - queryStack.add(query) - } - - if (query.isNotEmpty() && mode != Mode.SEARCH) { - Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply { - setAction(android.R.string.ok) { - cancelFetch() - clearGalleries() - currentPage = 0 - mode = Mode.SEARCH - queryStack.clear() - fetchGalleries(query, sortMode) - loadBlocks() - } - }.show() - } - - galleryIDs = null - - if (galleryIDs?.isActive == true) - return - - galleryIDs = CoroutineScope(Dispatchers.IO).async { - when(mode) { - Mode.SEARCH -> { - when { - query.isEmpty() and defaultQuery.isEmpty() -> { - when(sortMode) { - SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") - else -> getGalleryIDsFromNozomi(null, "index", "all") - }.also { - totalItems = it.size - } - } - else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also { - totalItems = it.size - } - } - } - Mode.HISTORY -> { - when { - query.isEmpty() -> { - histories.reversed().also { - totalItems = it.size - } - } - else -> { - val result = doSearch(query).sorted() - histories.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - Mode.DOWNLOAD -> { - val downloads = DownloadManager.getInstance(this@MainActivity).downloadFolderMap.keys.toList() - - when { - query.isEmpty() -> downloads.reversed().also { - totalItems = it.size - } - else -> { - val result = doSearch(query).sorted() - downloads.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - Mode.FAVORITE -> { - when { - query.isEmpty() -> favorites.reversed().also { - totalItems = it.size - } - else -> { - val result = doSearch(query).sorted() - favorites.reversed().filter { result.binarySearch(it) >= 0 }.also { - totalItems = it.size - } - } - } - } - }.toList() - } - } - - private fun loadBlocks() { - val perPage = Preferences["per_page", "25"].toInt() - - loadingJob = CoroutineScope(Dispatchers.IO).launch { - val galleryIDs = try { - galleryIDs!!.await().also { - if (it.isEmpty()) - throw Exception("No result") - } - } catch (e: Exception) { - if (e !is CancellationException) - FirebaseCrashlytics.getInstance().recordException(e) - - withContext(Dispatchers.Main) { - binding.contents.noresult.visibility = View.VISIBLE - binding.contents.progressbar.hide() - } - - return@launch - } - - launch(Dispatchers.Main) { - binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage) - } - - galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks -> - for (chunk in chunks) - chunk.map { galleryID -> - async { - Cache.getInstance(this@MainActivity, galleryID).getGalleryBlock()?.let { - galleryID - } - } - }.forEach { - it.await()?.also { - withContext(Dispatchers.Main) { - binding.contents.progressbar.hide() - - galleries.add(it) - binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1) - } - } - } - } - } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt new file mode 100644 index 00000000..00339db0 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/ContentType.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class ContentType { + SINGLE_PANE, DUAL_PANE +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt new file mode 100644 index 00000000..b4fdcb75 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/DevicePosture.kt @@ -0,0 +1,34 @@ +package xyz.quaver.pupil.ui.composable + +import android.graphics.Rect +import androidx.window.layout.FoldingFeature +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +sealed interface DevicePosture { + data object NormalPosture: DevicePosture + + data class BookPosture( + val hingePosition: Rect + ): DevicePosture + + data class Separating( + val hingePosition: Rect, + val orientation: FoldingFeature.Orientation + ): DevicePosture +} + +@OptIn(ExperimentalContracts::class) +fun isBookPosture(foldingFeature: FoldingFeature?): Boolean { + contract { returns(true) implies (foldingFeature != null) } + + return foldingFeature?.state == FoldingFeature.State.HALF_OPENED && + foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL +} + +@OptIn(ExperimentalContracts::class) +fun isSeparating(foldingFeature: FoldingFeature?): Boolean { + contract { returns(true) implies (foldingFeature != null) } + + return foldingFeature?.state == FoldingFeature.State.FLAT && foldingFeature.isSeparating +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt new file mode 100644 index 00000000..7b13c1af --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt @@ -0,0 +1,92 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.PermanentNavigationDrawer +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.compose.rememberNavController +import androidx.window.layout.DisplayFeature +import androidx.window.layout.FoldingFeature +import xyz.quaver.pupil.ui.viewmodel.MainUIState + +@Composable +fun PupilApp( + windowSize: WindowSizeClass, + displayFeatures: List, + uiState: MainUIState +) { + val navigationType: NavigationType + val contentType: ContentType + + val foldingFeature: FoldingFeature? = displayFeatures.filterIsInstance().firstOrNull() + val foldingDevicePosture = when { + isBookPosture(foldingFeature) -> DevicePosture.BookPosture(foldingFeature.bounds) + isSeparating(foldingFeature) -> DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation) + else -> DevicePosture.NormalPosture + } + + when (windowSize.widthSizeClass) { + WindowWidthSizeClass.Compact -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = ContentType.SINGLE_PANE + } + WindowWidthSizeClass.Medium -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) { + ContentType.DUAL_PANE + } else { + ContentType.SINGLE_PANE + } + } + WindowWidthSizeClass.Expanded -> { + navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) { + NavigationType.NAVIGATION_RAIL + } else { + NavigationType.PERMANENT_NAVIGATION_DRAWER + } + contentType = ContentType.DUAL_PANE + } + else -> { + navigationType = NavigationType.NAVIGATION_RAIL + contentType = ContentType.SINGLE_PANE + } + } + + val navigationContentPosition = when (windowSize.heightSizeClass) { + WindowHeightSizeClass.Compact -> NavigationContentPosition.TOP + WindowHeightSizeClass.Medium, + WindowHeightSizeClass.Expanded -> NavigationContentPosition.CENTER + else -> NavigationContentPosition.TOP + } + + PupilNavigationWrapper( + navigationType, + contentType, + navigationContentPosition + ) + +} + +@Composable +private fun PupilNavigationWrapper( + navigationType: NavigationType, + contentType: ContentType, + navigationContentPosition: NavigationContentPosition +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val coroutineScope = rememberCoroutineScope() + + if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) { + PermanentNavigationDrawer(drawerContent = { + PermanentNavigationDrawerContent( + navigationContentPosition = navigationContentPosition + ) + }) { +// PupilMain() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt new file mode 100644 index 00000000..d32c2f31 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/MainNavigationActions.kt @@ -0,0 +1,38 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.ui.graphics.vector.ImageVector +import xyz.quaver.pupil.R + +data class MainDestination( + val route: String, + val icon: ImageVector, + val textId: Int +) + +val mainDestinations = listOf( + MainDestination( + "search", + Icons.Default.Search, + R.string.main_destination_search + ), + MainDestination( + "history", + Icons.Default.History, + R.string.main_destination_history + ), + MainDestination( + "downloads", + Icons.Default.Download, + R.string.main_destination_downloads + ), + MainDestination( + "favorites", + Icons.Default.Star, + R.string.main_destination_favorites + ), +) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt new file mode 100644 index 00000000..1b10b675 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationContentPosition.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class NavigationContentPosition { + TOP, CENTER +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt new file mode 100644 index 00000000..2513c175 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationDrawerContent.kt @@ -0,0 +1,59 @@ +package xyz.quaver.pupil.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PermanentDrawerSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import xyz.quaver.pupil.R + +@Composable +fun PermanentNavigationDrawerContent( + navigationContentPosition: NavigationContentPosition +) { + PermanentDrawerSheet( + modifier = Modifier.sizeIn(minWidth = 200.dp, maxWidth = 300.dp), + drawerContainerColor = MaterialTheme.colorScheme.inverseOnSurface + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.inverseOnSurface) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(32.dp), + painter = painterResource(R.drawable.app_icon), + tint = Color.Unspecified, + contentDescription = "app icon" + ) + Text( + modifier = Modifier.padding(16.dp), + text = "Pupil", + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.primary + ) + } + } + Column( + + ) { + Text("Help") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt new file mode 100644 index 00000000..73796fab --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/NavigationType.kt @@ -0,0 +1,5 @@ +package xyz.quaver.pupil.ui.composable + +enum class NavigationType { + NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt deleted file mode 100644 index 7509ea70..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/GalleryDialog.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.dialog - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout.LayoutParams -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.quaver.pupil.hitomi.Gallery -import xyz.quaver.pupil.hitomi.getGallery -import xyz.quaver.pupil.R -import xyz.quaver.pupil.adapters.GalleryBlockAdapter -import xyz.quaver.pupil.adapters.ThumbnailPageAdapter -import xyz.quaver.pupil.databinding.* -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.ui.ReaderActivity -import xyz.quaver.pupil.ui.view.TagChip -import xyz.quaver.pupil.util.ItemClickSupport -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.wordCapitalize -import java.util.* -import kotlin.collections.ArrayList - -class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(context) { - - val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() - - private lateinit var binding: GalleryDialogBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = GalleryDialogBinding.inflate(layoutInflater) - setContentView(binding.root) - - window?.attributes.apply { - this ?: return@apply - - width = LayoutParams.MATCH_PARENT - height = LayoutParams.MATCH_PARENT - } - - with(binding.fab) { - setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right)) - setOnClickListener { - context.startActivity(Intent(context, ReaderActivity::class.java).apply { - putExtra("galleryID", galleryID) - }) - } - } - - CoroutineScope(Dispatchers.IO).launch { - try { - val gallery = getGallery(galleryID) - - launch (Dispatchers.Main) { - binding.progressbar.visibility = View.GONE - binding.title.text = gallery.title - binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } - - with(binding.type) { - text = gallery.type.wordCapitalize() - setOnClickListener { - gallery.type.let { - when (it) { - "artist CG" -> "artistcg" - "game CG" -> "gamecg" - else -> it - } - }.let { - onChipClickedHandler.forEach { handler -> - handler.invoke(Tag("type", it)) - } - } - } - } - - binding.cover.showImage(Uri.parse(gallery.cover)) - - addDetails(gallery) - addThumbnails(gallery) - addRelated(gallery) - } - } catch (e: Exception) { - Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply { - if (Locale.getDefault().language == "ko") - setAction(context.getText(R.string.https_text)) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https)))) - } - }.show() - } - } - } - - private fun addDetails(gallery: Gallery) { - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_details) - - listOf( - R.string.gallery_artists, - R.string.gallery_groups, - R.string.gallery_language, - R.string.gallery_series, - R.string.gallery_characters, - R.string.gallery_tags - ).zip( - listOf( - gallery.artists.map { Tag("artist", it) }, - gallery.groups.map { Tag("group", it) }, - listOf(gallery.language).map { Tag("language", it) }, - gallery.series.map { Tag("series", it) }, - gallery.characters.map { Tag("character", it) }, - gallery.tags.sortedBy { - val tag = Tag.parse(it) - - if (favoriteTags.contains(tag)) - -1 - else - when(Tag.parse(it).area) { - "female" -> 0 - "male" -> 1 - else -> 2 - } - }.map { - Tag.parse(it).let { tag -> - when { - tag.area != null -> tag - else -> Tag("tag", it) - } - } - } - ) - ).filter { - (_, content) -> content.isNotEmpty() - }.forEach { (title, content) -> - GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply { - type.setText(title) - - content.forEach { tag -> - tags.addView( - TagChip(context, tag).apply { - setOnClickListener { - onChipClickedHandler.forEach { handler -> - handler.invoke(tag) - } - } - } - ) - } - } - } - } - } - - private fun addThumbnails(gallery: Gallery) { - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_thumbnails) - - val pager = ViewPager2(context).apply { - adapter = ThumbnailPageAdapter(gallery.thumbnails) - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - } - - contents.addView( - pager, - LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - ) - - // TODO: Change to direct allocation - GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply { - dotindicator.setViewPager2(pager) - } - } - } - - private fun addRelated(gallery: Gallery) { - val galleries = mutableListOf() - - val adapter = GalleryBlockAdapter(galleries).apply { - onChipClickedHandler.add { tag -> - this@GalleryDialog.onChipClickedHandler.forEach { handler -> - handler.invoke(tag) - } - } - } - - GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply { - type.setText(R.string.gallery_related) - - contents.addView(RecyclerView(context).apply { - layoutManager = LinearLayoutManager(context) - this.adapter = adapter - - ItemClickSupport.addTo(this).apply { - onItemClickListener = { _, position, _ -> - context.startActivity(Intent(context, ReaderActivity::class.java).apply { - putExtra("galleryID", galleries[position]) - }) - } - onItemLongClickListener = { _, position, _ -> - GalleryDialog(context, galleries[position]).apply { - onChipClickedHandler.add { tag -> - this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) } - } - }.show() - - true - } - } - }) - - CoroutineScope(Dispatchers.IO).launch { - gallery.related.forEach { galleryID -> - Cache.getInstance(context, galleryID).getGalleryBlock()?.let { - galleries.add(galleryID) - } - } - - withContext(Dispatchers.Main) { - adapter.notifyDataSetChanged() - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt deleted file mode 100644 index b4fbf5b6..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageStorageFragment.kt +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.fragment - -import android.graphics.ColorFilter -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.swiperefreshlayout.widget.CircularProgressDrawable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import xyz.quaver.io.FileX -import xyz.quaver.io.SAFileX -import xyz.quaver.io.util.deleteRecursively -import xyz.quaver.io.util.getChild -import xyz.quaver.io.util.readText -import xyz.quaver.io.util.writeText -import xyz.quaver.pupil.R -import xyz.quaver.pupil.histories -import xyz.quaver.pupil.hitomi.json -import xyz.quaver.pupil.util.byteToString -import xyz.quaver.pupil.util.downloader.Cache -import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.downloader.Metadata -import java.io.File -import kotlin.math.roundToInt - -class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { - - private var job: Job? = null - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey) - - initPreferences() - } - - override fun onPreferenceClick(preference: Preference): Boolean { - val context = context ?: return false - - with(preference) { - when (key) { - "delete_cache" -> { - val dir = File(context.cacheDir, "imageCache") - - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_cache_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - if (dir.exists()) - dir.deleteRecursively() - - Cache.instances.clear() - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - "recover_downloads" -> { - val density = context.resources.displayMetrics.density - this.icon = object: CircularProgressDrawable(context) { - override fun getIntrinsicHeight() = (24*density).roundToInt() - override fun getIntrinsicWidth() = (24*density).roundToInt() - }.apply { - setStyle(CircularProgressDrawable.DEFAULT) - colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN) - start() - } - - val downloadManager = DownloadManager.getInstance(context) - - val downloadFolderMap = downloadManager.downloadFolderMap - - downloadFolderMap.clear() - - downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder -> - val metadataFile = FileX(context, folder, ".metadata") - - if (!metadataFile.exists()) return@forEach - - val metadata = metadataFile.readText()?.let { - runCatching { - json.decodeFromString(it) - }.getOrNull() - } ?: return@forEach - - val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach - - downloadFolderMap[galleryID] = folder.name - } - - downloadManager.downloadFolderMap.putAll(downloadFolderMap) - val downloads = FileX(context, downloadManager.downloadFolder, ".download") - - if (!downloads.exists()) downloads.createNewFile() - downloads.writeText(Json.encodeToString(downloadFolderMap)) - - this.icon = null - Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show() - } - "delete_downloads" -> { - val dir = DownloadManager.getInstance(context).downloadFolder - - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_downloads_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - CoroutineScope(Dispatchers.IO).launch { - job?.cancel() - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage_loading) - } - - if (dir.exists()) - dir.listFiles()?.forEach { - when (it) { - is FileX -> it.deleteRecursively() - else -> it.deleteRecursively() - } - } - - job = launch { - var size = 0L - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - } - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - "clear_history" -> { - AlertDialog.Builder(context).apply { - setTitle(R.string.warning) - setMessage(R.string.settings_clear_history_alert_message) - setPositiveButton(android.R.string.ok) { _, _ -> - histories.clear() - summary = context.getString(R.string.settings_clear_history_summary, histories.size) - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - } - else -> return false - } - } - - return true - } - - private fun initPreferences() { - val context = context ?: return - - with(findPreference("delete_cache")) { - this ?: return@with - - val dir = File(context.cacheDir, "imageCache") - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - size += it.length() - - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - } - } - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("delete_downloads")) { - this ?: return@with - - val dir = DownloadManager.getInstance(context).downloadFolder - - summary = context.getString(R.string.settings_storage_usage, byteToString(0)) - job?.cancel() - job = CoroutineScope(Dispatchers.IO).launch { - var size = 0L - - dir.walk().forEach { - launch(Dispatchers.Main) { - summary = context.getString(R.string.settings_storage_usage, byteToString(size)) - } - - size += it.length() - } - } - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("clear_history")) { - this ?: return@with - - summary = context.getString(R.string.settings_clear_history_summary, histories.size) - - onPreferenceClickListener = this@ManageStorageFragment - } - - with(findPreference("recover_downloads")) { - this ?: return@with - - onPreferenceClickListener = this@ManageStorageFragment - } - } - - override fun onDestroy() { - job?.cancel() - super.onDestroy() - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt new file mode 100644 index 00000000..802b4d5f --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt @@ -0,0 +1,67 @@ +package xyz.quaver.pupil.ui.theme +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFF006688) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFC2E8FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001E2B) +val md_theme_light_secondary = Color(0xFF4E616D) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFD1E5F3) +val md_theme_light_onSecondaryContainer = Color(0xFF091E28) +val md_theme_light_tertiary = Color(0xFF5F5A7D) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFE5DEFF) +val md_theme_light_onTertiaryContainer = Color(0xFF1C1736) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFBFCFE) +val md_theme_light_onBackground = Color(0xFF191C1E) +val md_theme_light_surface = Color(0xFFFBFCFE) +val md_theme_light_onSurface = Color(0xFF191C1E) +val md_theme_light_surfaceVariant = Color(0xFFDCE3E9) +val md_theme_light_onSurfaceVariant = Color(0xFF40484D) +val md_theme_light_outline = Color(0xFF71787D) +val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3) +val md_theme_light_inverseSurface = Color(0xFF2E3133) +val md_theme_light_inversePrimary = Color(0xFF75D1FF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF006688) +val md_theme_light_outlineVariant = Color(0xFFC0C7CD) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFF75D1FF) +val md_theme_dark_onPrimary = Color(0xFF003548) +val md_theme_dark_primaryContainer = Color(0xFF004D67) +val md_theme_dark_onPrimaryContainer = Color(0xFFC2E8FF) +val md_theme_dark_secondary = Color(0xFFB5C9D7) +val md_theme_dark_onSecondary = Color(0xFF20333D) +val md_theme_dark_secondaryContainer = Color(0xFF364954) +val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F3) +val md_theme_dark_tertiary = Color(0xFFC9C2EA) +val md_theme_dark_onTertiary = Color(0xFF312C4C) +val md_theme_dark_tertiaryContainer = Color(0xFF474364) +val md_theme_dark_onTertiaryContainer = Color(0xFFE5DEFF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF191C1E) +val md_theme_dark_onBackground = Color(0xFFE1E2E5) +val md_theme_dark_surface = Color(0xFF191C1E) +val md_theme_dark_onSurface = Color(0xFFE1E2E5) +val md_theme_dark_surfaceVariant = Color(0xFF40484D) +val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD) +val md_theme_dark_outline = Color(0xFF8A9297) +val md_theme_dark_inverseOnSurface = Color(0xFF191C1E) +val md_theme_dark_inverseSurface = Color(0xFFE1E2E5) +val md_theme_dark_inversePrimary = Color(0xFF006688) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF75D1FF) +val md_theme_dark_outlineVariant = Color(0xFF40484D) +val md_theme_dark_scrim = Color(0xFF000000) + + +val seed = Color(0xFF4FC3F7) diff --git a/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt new file mode 100644 index 00000000..9fe02e7a --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt @@ -0,0 +1,90 @@ +package xyz.quaver.pupil.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun AppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt deleted file mode 100644 index 93fea583..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/FloatingSearchView.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view - -import android.content.Context -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.drawable.Animatable -import android.text.Editable -import android.text.TextWatcher -import android.util.AttributeSet -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.swiperefreshlayout.widget.CircularProgressDrawable -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import xyz.quaver.floatingsearchview.FloatingSearchView -import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion -import xyz.quaver.floatingsearchview.util.view.SearchInputView -import xyz.quaver.pupil.R -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.* -import java.util.* - -class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - FloatingSearchView(context, attrs), - FloatingSearchView.OnSearchListener, - TextWatcher -{ - private val searchInputView = findViewById(R.id.search_bar_text) - - var onHistoryDeleteClickedListener: ((String) -> Unit)? = null - var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null - - init { - searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions - - searchInputView.addTextChangedListener(this) - onSearchListener = this - onBindSuggestionCallback = { binding, item, itemPosition -> - onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition) - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - - } - - override fun afterTextChanged(s: Editable?) { - s ?: return - - if (s.any { it.isUpperCase() }) - s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault())) - } - - override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { - when (searchSuggestion) { - is TagSuggestion -> { - val tag = "${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")}" - with(searchInputView.text!!) { - delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ') + 1, length) - - if (!this.contains(tag)) - append("$tag ") - } - } - is Suggestion -> { - with(searchInputView.text!!) { - clear() - append(searchSuggestion.body) - } - } - is FavoriteHistorySwitch -> onFavoriteHistorySwitchClickListener?.invoke() - } - } - - override fun onSearchAction(currentQuery: String?) {} - - fun onBindSuggestion( - suggestionView: View?, - leftIcon: ImageView?, - textView: TextView?, - item: SearchSuggestion?, - itemPosition: Int - ) { - when(item) { - is TagSuggestion -> { - val tag = "${item.n}:${item.s}" - - leftIcon?.setImageDrawable( - ResourcesCompat.getDrawable( - resources, - when(item.n) { - "female" -> R.drawable.gender_female - "male" -> R.drawable.gender_male - "language" -> R.drawable.translate - "group" -> R.drawable.account_group - "character" -> R.drawable.account_star - "series" -> R.drawable.book_open - "artist" -> R.drawable.brush - else -> R.drawable.tag - }, - context.theme) - ) - - with(suggestionView?.findViewById(R.id.right_icon)) { - this ?: return@with - - if (favoriteTags.contains(Tag.parse(tag))) - setImageResource(R.drawable.ic_star_filled) - else - setImageResource(R.drawable.ic_star_empty) - - visibility = View.VISIBLE - rotation = 0f - - isEnabled = true - isClickable = true - - setOnClickListener { - val tag = Tag.parse(tag) - - if (favoriteTags.contains(tag)) { - setImageResource(R.drawable.ic_star_empty) - favoriteTags.remove(tag) - } - else { - setImageDrawable( - AnimatedVectorDrawableCompat.create(context, - R.drawable.avd_star - )) - (drawable as Animatable).start() - - favoriteTags.add(tag) - } - } - } - - if (item.t > 0) { - (suggestionView as? LinearLayout)?.let { - val count = it.findViewById(R.id.count) - if (count == null) - it.addView( - LayoutInflater.from(context).inflate(R.layout.suggestion_count, suggestionView, false) - .apply { - this as TextView - - text = item.t.toString() - }, 2 - ) - else - count.text = item.t.toString() - } - } - } - is FavoriteHistorySwitch -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.swap_horizontal, context.theme)) - } - is Suggestion -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.history, context.theme)) - - with(suggestionView?.findViewById(R.id.right_icon)) { - this ?: return@with - - setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.delete, context.theme)) - - visibility = View.VISIBLE - rotation = 0f - - isEnabled = true - isClickable = true - - setOnClickListener { - onHistoryDeleteClickedListener?.invoke(item.body) - } - } - } - is LoadingSuggestion -> { - leftIcon?.setImageDrawable(CircularProgressDrawable(context).also { - it.setStyle(CircularProgressDrawable.DEFAULT) - it.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN) - it.start() - }) - } - is NoResultSuggestion -> { - leftIcon?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.close, context.theme)) - } - } - } -} diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java b/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java deleted file mode 100644 index 2c37eb83..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/MainView.java +++ /dev/null @@ -1,462 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.os.Vibrator; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.view.NestedScrollingChild; -import androidx.core.view.NestedScrollingChildHelper; -import androidx.core.view.NestedScrollingParent; -import androidx.core.view.NestedScrollingParentHelper; -import androidx.core.view.ViewCompat; -import androidx.core.widget.TextViewCompat; - -import xyz.quaver.pupil.R; - -@SuppressWarnings("NullableProblems") -public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent { - - private static final int PAGE_TURN_LAYOUT_SIZE = 48; - private static final int PAGE_TURN_ANIM_DURATION = 500; - private static final int PREV_OFFSET = 64; - private static final int RIPPLE_GIVE = 4; - - private final float adjustedPageTurnLayoutSize; - private final float adjustedPrevOffset; - private final float adjustedRippleGive; - - final private NestedScrollingParentHelper mNestedScrollingParentHelper; - final private NestedScrollingChildHelper mNestedScrollingChildHelper; - - final private Vibrator mVibrator; - - private View mTarget; - - private TextView mPrev; - private TextView mNext; - - private final Paint mRipplePaint = new Paint(); - private final Rect mRippleBound = new Rect(); - - private int mRippleSize = 0; - private final int mRippleTargetSize; - private final ValueAnimator mRippleAnimator = new ValueAnimator(); - - private int mCurrentOverScroll = 0; - - private int mCurrentPage = 1; - private boolean mShowPrev; - private boolean mShowNext; - - private OnPageTurnListener mOnPageTurnListener; - - public MainView(@NonNull Context context) { - this(context, null); - } - - public MainView(@NonNull Context context, AttributeSet attr) { - this(context, attr, 0); - } - - public MainView(@NonNull Context context, AttributeSet attr, int defStyle) { - super(context, attr, defStyle); - - setWillNotDraw(false); - - DisplayMetrics metrics = getResources().getDisplayMetrics(); - - adjustedPageTurnLayoutSize = PAGE_TURN_LAYOUT_SIZE * metrics.density; - adjustedPrevOffset = PREV_OFFSET * metrics.density; - adjustedRippleGive = RIPPLE_GIVE * metrics.density; - - mRippleTargetSize = metrics.widthPixels; - - mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); - mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); - - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - - mRippleAnimator.addUpdateListener(animation -> { - mRippleSize = (int) animation.getAnimatedValue(); - invalidate(); - }); - mRippleAnimator.setDuration(PAGE_TURN_ANIM_DURATION); - - initPageTurnView(); - } - - public void setCurrentPage(int currentPage, boolean showNext) { - mCurrentPage = currentPage; - - mShowPrev = currentPage > 1; - mShowNext = showNext; - - mPrev.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage-1)); - mNext.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage+1)); - } - - public void setOnPageTurnListener(OnPageTurnListener listener) { - mOnPageTurnListener = listener; - } - - private void initPageTurnView() { - TextView prev = new TextView(getContext()); - TextView next = new TextView(getContext()); - - prev.setGravity(Gravity.CENTER_VERTICAL); - next.setGravity(Gravity.CENTER_VERTICAL); - - prev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.navigate_prev, 0, 0, 0); - next.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.navigate_next, 0); - - TextViewCompat.setCompoundDrawableTintList(prev, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent)); - TextViewCompat.setCompoundDrawableTintList(next, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent)); - - prev.setVisibility(View.INVISIBLE); - next.setVisibility(View.INVISIBLE); - - mPrev = prev; - mNext = next; - - addView(mPrev); - addView(mNext); - - setCurrentPage(1, false); - } - - private void ensureTarget() { - if (mTarget == null) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - - if (!child.equals(mNext) && !child.equals(mPrev)) { - mTarget = child; - break; - } - } - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - final int width = getMeasuredWidth(); - final int height = getMeasuredHeight(); - - if (getChildCount() == 0) - return; - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mTarget.layout( - getPaddingLeft(), - getPaddingTop(), - width - getPaddingRight(), - height - getPaddingBottom() - ); - - final int prevWidth = mPrev.getMeasuredWidth(); - mPrev.layout( - width / 2 - prevWidth / 2, - getPaddingTop() + (int) adjustedPrevOffset, - width / 2 + prevWidth / 2, - getPaddingTop() + (int) adjustedPrevOffset + mPrev.getMeasuredHeight() - ); - - final int nextWidth = mNext.getMeasuredWidth(); - mNext.layout( - width / 2 - nextWidth / 2, - height - getPaddingBottom() - mNext.getMeasuredHeight(), - width / 2 + nextWidth / 2, - height - getPaddingBottom() - ); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mTarget.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY) - ); - - mPrev.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY) - ); - - mNext.measure( - MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY) - ); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - if (mCurrentOverScroll == 0) - return; - - if (mCurrentOverScroll > 0) { - mRippleBound.set( - getPaddingLeft(), - (int) (getPaddingTop() - adjustedRippleGive), - getMeasuredWidth() - getPaddingRight(), - (int) (getPaddingTop() + adjustedPrevOffset + mPrev.getMeasuredHeight() + adjustedRippleGive) - ); - } - - if (mCurrentOverScroll < 0) { - final int height = getMeasuredHeight(); - mRippleBound.set( - getPaddingLeft(), - (int) (height - getPaddingBottom() - mNext.getMeasuredHeight() - adjustedRippleGive), - getMeasuredWidth() - getPaddingRight(), - height - getPaddingBottom() - ); - } - - mRipplePaint.reset(); - mRipplePaint.setStyle(Paint.Style.FILL); - - int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - switch (currentNightMode) { - case Configuration.UI_MODE_NIGHT_YES: - mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_700)); - break; - case Configuration.UI_MODE_NIGHT_NO: - mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_300)); - break; - } - - canvas.drawCircle( - (mRippleBound.left + mRippleBound.right) / 2F, - mCurrentOverScroll > 0 ? mRippleBound.bottom : mRippleBound.top, - mRippleSize, - mRipplePaint - ); - } - - private void onOverscroll(int overscroll) { - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mCurrentOverScroll = overscroll; - - if (overscroll > 0) { - mPrev.setVisibility(View.VISIBLE); - mNext.setVisibility(View.INVISIBLE); - } else if (overscroll < 0) { - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.VISIBLE); - } else { - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.INVISIBLE); - } - - if (Math.abs(overscroll) >= adjustedPageTurnLayoutSize) { - if (!mRippleAnimator.isStarted() && mRippleSize != mRippleTargetSize) { - mVibrator.vibrate(10); - - mRippleAnimator.setIntValues(mRippleSize, mRippleTargetSize); - mRippleAnimator.start(); - } - } else { - if (!mRippleAnimator.isStarted() && mRippleSize != 0) { - mRippleAnimator.setIntValues(mRippleSize, 0); - mRippleAnimator.start(); - } - } - - float clippedOverScrollTop = (overscroll > 0 ? 1 : -1) * Math.min(Math.abs(overscroll), adjustedPageTurnLayoutSize); - mTarget.setTranslationY(clippedOverScrollTop); - } - - private void onOverscrollEnd(int overscroll) { - if (mTarget == null) - ensureTarget(); - if (mTarget == null) - return; - - mRippleAnimator.cancel(); - mRippleAnimator.setIntValues(mRippleSize, 0); - mRippleAnimator.start(); - - mPrev.setVisibility(View.INVISIBLE); - mNext.setVisibility(View.INVISIBLE); - - ViewCompat.animate(mTarget) - .setDuration(PAGE_TURN_ANIM_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .translationY(0); - - if (Math.abs(overscroll) > adjustedPageTurnLayoutSize && mOnPageTurnListener != null) { - if (overscroll > 0) - mOnPageTurnListener.onPrev(mCurrentPage-1); - if (overscroll < 0) - mOnPageTurnListener.onNext(mCurrentPage+1); - } - } - - // NestedScrollingParent - - private int mTotalUnconsumed = 0; - - @Override - public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { - return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; - } - - @Override - public void onNestedScrollAccepted(View child, View target, int axes) { - mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); - startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); - - mTotalUnconsumed = 0; - } - - @Override - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { - if (mTotalUnconsumed != 0 && dy > 0 == mTotalUnconsumed > 0) { - if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) { - consumed[1] = dy - mTotalUnconsumed; - mTotalUnconsumed = 0; - } else { - mTotalUnconsumed -= dy; - consumed[1] = dy; - } - - onOverscroll(mTotalUnconsumed); - } - - final int[] parentConsumed = new int[2]; - if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { - consumed[0] += parentConsumed[0]; - consumed[1] += parentConsumed[1]; - } - } - - @Override - public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { - final int[] mParentOffsetInWindow = new int[2]; - dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow); - - final int dy = dyUnconsumed + mParentOffsetInWindow[1]; - - if (mTotalUnconsumed == 0 && ((dy < 0 && !mShowPrev) || (dy > 0 && !mShowNext))) - return; - - if (dy != 0) { - mTotalUnconsumed -= dy; - onOverscroll(mTotalUnconsumed); - } - } - - @Override - public void onStopNestedScroll(View child) { - mNestedScrollingParentHelper.onStopNestedScroll(child); - - if (Math.abs(mTotalUnconsumed) > 0) { - onOverscrollEnd(mTotalUnconsumed); - mTotalUnconsumed = 0; - } - - stopNestedScroll(); - } - - // NestedScrollingChild - - @Override - public void setNestedScrollingEnabled(boolean enabled) { - mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); - } - - @Override - public boolean isNestedScrollingEnabled() { - return mNestedScrollingChildHelper.isNestedScrollingEnabled(); - } - - @Override - public boolean startNestedScroll(int axes) { - return mNestedScrollingChildHelper.startNestedScroll(axes); - } - - @Override - public void stopNestedScroll() { - mNestedScrollingChildHelper.stopNestedScroll(); - } - - @Override - public boolean hasNestedScrollingParent() { - return mNestedScrollingChildHelper.hasNestedScrollingParent(); - } - - @Override - public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { - return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); - } - - @Override - public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { - return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); - } - - @Override - public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { - return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); - } - - @Override - public boolean dispatchNestedPreFling(float velocityX, float velocityY) { - return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); - } - - public interface OnPageTurnListener { - void onPrev(int page); - void onNext(int page); - } -} diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt deleted file mode 100644 index 0b5a32b1..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/ProgressCard.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.quaver.pupil.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.cardview.widget.CardView -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.DrawableCompat -import xyz.quaver.pupil.R -import xyz.quaver.pupil.databinding.ProgressCardViewBinding - -class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) { - - enum class Type { - LOADING, - CACHE, - DOWNLOAD - } - - var type: Type = Type.LOADING - set(value) { - field = value - - when (field) { - Type.LOADING -> R.color.colorAccent - Type.CACHE -> R.color.material_blue_700 - Type.DOWNLOAD -> R.color.material_green_a700 - }.let { - val color = ContextCompat.getColor(context, it) - DrawableCompat.setTint(binding.progressbar.progressDrawable, color) - } - } - - var progress: Int - get() = binding.progressbar.progress - set(value) { - binding.progressbar.progress = value - } - var max: Int - get() = binding.progressbar.max - set(value) { - binding.progressbar.max = value - - binding.progressbar.visibility = - if (value == 0) - GONE - else - VISIBLE - } - - val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this) - - init { - binding.content.setOnClickListener { - performClick() - } - - binding.content.setOnLongClickListener { - performLongClick() - } - } - - override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { - if (childCount == 0) - super.addView(child, index, params) - else - binding.content.addView(child, index, params) - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt deleted file mode 100644 index 917979da..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view - -import android.annotation.SuppressLint -import android.content.Context -import androidx.core.content.ContextCompat -import com.google.android.material.chip.Chip -import xyz.quaver.pupil.R -import xyz.quaver.pupil.favoriteTags -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.util.translations -import xyz.quaver.pupil.util.wordCapitalize - -@SuppressLint("ViewConstructor") -class TagChip(context: Context, _tag: Tag) : Chip(context) { - - val tag: Tag = - _tag.let { - when { - it.area != null -> it - else -> Tag("tag", _tag.tag) - } - } - - private val languages = context.resources.getStringArray(R.array.languages).map { - it.split("|").let { split -> - Pair(split[0], split[1]) - } - }.toMap() - - init { - when(tag.area) { - "male" -> { - setChipBackgroundColorResource(R.color.material_blue_700) - setTextColor(ContextCompat.getColor(context, android.R.color.white)) - setCloseIconTintResource(android.R.color.white) - chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_male_white) - } - "female" -> { - setChipBackgroundColorResource(R.color.material_pink_600) - setTextColor(ContextCompat.getColor(context, android.R.color.white)) - setCloseIconTintResource(android.R.color.white) - chipIcon = ContextCompat.getDrawable(context, R.drawable.gender_female_white) - } - } - - if (favoriteTags.contains(tag)) - setChipBackgroundColorResource(R.color.material_orange_500) - - isCloseIconVisible = true - closeIcon = ContextCompat.getDrawable(context, - if (favoriteTags.contains(tag)) - R.drawable.ic_star_filled - else - R.drawable.ic_star_empty - ) - - setOnCloseIconClickListener { - if (favoriteTags.contains(tag)) { - favoriteTags.remove(tag) - closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty) - - when(tag.area) { - "male" -> setChipBackgroundColorResource(R.color.material_blue_700) - "female" -> setChipBackgroundColorResource(R.color.material_pink_600) - else -> chipBackgroundColor = null - } - } else { - favoriteTags.add(tag) - closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled) - setChipBackgroundColorResource(R.color.material_orange_500) - } - } - - text = when (tag.area) { - "language" -> languages[tag.tag] - else -> (translations[tag.tag] ?: tag.tag).wordCapitalize() - } - - setEnsureMinTouchTargetSize(false) - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt deleted file mode 100644 index 3c3dd136..00000000 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChipGroup.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Pupil, Hitomi.la viewer for Android - * Copyright (C) 2020 tom5079 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package xyz.quaver.pupil.ui.view - -import android.content.Context -import android.content.res.TypedArray -import android.util.AttributeSet -import android.util.Log -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import kotlinx.coroutines.* -import xyz.quaver.pupil.R -import xyz.quaver.pupil.types.Tag -import xyz.quaver.pupil.types.Tags - -class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet by tags { - - object Defaults { - const val maxChipSize = 10 - } - - var maxChipSize: Int = Defaults.maxChipSize - set(value) { - field = value - - refresh() - } - - private val moreView = Chip(context).apply { - text = "…" - - setEnsureMinTouchTargetSize(false) - - setOnClickListener { - removeView(this) - - for (i in maxChipSize until tags.size) { - val tag = tags.elementAt(i) - - addView(TagChip(context, tag).apply { - setOnClickListener { - onClickListener?.invoke(tag) - } - }) - } - } - } - - var onClickListener: ((Tag) -> Unit)? = null - - private fun applyAttributes(attr: TypedArray) { - maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize) - } - - private var refreshJob: Job? = null - fun refresh() { - refreshJob?.cancel() - this.removeAllViews() - - refreshJob = CoroutineScope(Dispatchers.Main).launch { - tags.take(maxChipSize).map { - CoroutineScope(Dispatchers.Default).async { - TagChip(context, it).apply { - setOnClickListener { - onClickListener?.invoke(this.tag) - } - } - } - }.forEach { - addView(it.await()) - } - - if (maxChipSize > 0 && tags.size > maxChipSize) - addView(moreView) - } - } - - init { - applyAttributes(context.obtainStyledAttributes(attr, R.styleable.TagChipGroup)) - - refresh() - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt new file mode 100644 index 00000000..b2c84488 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt @@ -0,0 +1,14 @@ +package xyz.quaver.pupil.ui.viewmodel + +import androidx.lifecycle.ViewModel +import xyz.quaver.pupil.networking.SearchQuery +import xyz.quaver.pupil.ui.composable.MainRoutes + +class MainViewModel : ViewModel() { + val uiState: MainUIState = MainUIState() +} + +data class MainUIState( + val route: MainRoutes = MainRoutes.SEARCH, + val query: SearchQuery? = null +) \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt index 85c407c8..0bcba433 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/Cache.kt @@ -190,7 +190,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW .header("Referer", "https://hitomi.la/") .build() - client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } + client.newCall(request).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.bytes() } }.getOrNull()?.let { thumbnail -> kotlin.runCatching { cacheFolder.getChild(".thumbnail").also { if (!it.exists()) diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt index ed8f088f..77349b4b 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt @@ -83,10 +83,10 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con @Synchronized fun isDownloading(galleryID: Int): Boolean { - val isThisGallery: (Call) -> Boolean = { !it.isCanceled && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } + val isThisGallery: (Call) -> Boolean = { !it.isCanceled() && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } return downloadFolderMap.containsKey(galleryID) - && client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) } + && client.dispatcher.let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) } } @Synchronized diff --git a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt index 778ff2ee..20947d3c 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt @@ -47,7 +47,7 @@ data class ProxyInfo( Authenticator { _, response -> val credential = Credentials.basic(username, password) - response.request().newBuilder() + response.request.newBuilder() .header("Proxy-Authorization", credential) .build() } diff --git a/app/src/main/java/xyz/quaver/pupil/util/translation.kt b/app/src/main/java/xyz/quaver/pupil/util/translation.kt index 33b7ba05..61b92990 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/translation.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/translation.kt @@ -44,10 +44,10 @@ fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch { translations = emptyMap() kotlin.runCatching { translations = Json.decodeFromString>(client.newCall( - Request.Builder() - .url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json") - .build() - ).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() } + Request.Builder() + .url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json") + .build() + ).execute().also { if (it.code != 200) return@launch }.body?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() } } } @@ -58,7 +58,7 @@ fun getAvailableLanguages(): List { Request.Builder() .url(filesURL) .build() - ).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: return emptyList()) + ).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.string() } ?: return emptyList()) return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull { val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' } diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt index e07a4e16..e8b817aa 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -193,7 +193,7 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: (( override fun onResponse(call: Call, response: Response) { kotlin.runCatching { - val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]") + val data = Json.parseToJsonElement(response.also { if (it.code != 200) throw IOException() }.body.use { it?.string() } ?: "[]") when (data) { is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int }) diff --git a/app/src/main/res/drawable-hdpi/ic_email.png b/app/src/main/res/drawable-hdpi/ic_email.png deleted file mode 100644 index 6cf75e2a..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_email.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_help.png b/app/src/main/res/drawable-hdpi/ic_help.png deleted file mode 100644 index 64941638..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_home.png b/app/src/main/res/drawable-hdpi/ic_home.png deleted file mode 100644 index caccfac0..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_home.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_settings.png b/app/src/main/res/drawable-hdpi/ic_settings.png deleted file mode 100644 index 24ba874a..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_email.png b/app/src/main/res/drawable-mdpi/ic_email.png deleted file mode 100644 index d305e584..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_email.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_help.png b/app/src/main/res/drawable-mdpi/ic_help.png deleted file mode 100644 index b2b8c636..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_home.png b/app/src/main/res/drawable-mdpi/ic_home.png deleted file mode 100644 index 1eccf9f3..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_home.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_settings.png b/app/src/main/res/drawable-mdpi/ic_settings.png deleted file mode 100644 index c891ad02..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_email.png b/app/src/main/res/drawable-xhdpi/ic_email.png deleted file mode 100644 index 6854119d..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_email.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_help.png b/app/src/main/res/drawable-xhdpi/ic_help.png deleted file mode 100644 index 6ef7a89d..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home.png b/app/src/main/res/drawable-xhdpi/ic_home.png deleted file mode 100644 index cea7b6c5..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_home.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_settings.png b/app/src/main/res/drawable-xhdpi/ic_settings.png deleted file mode 100644 index 92d74d24..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_email.png b/app/src/main/res/drawable-xxhdpi/ic_email.png deleted file mode 100644 index ed17daf2..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_email.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_help.png b/app/src/main/res/drawable-xxhdpi/ic_help.png deleted file mode 100644 index 760e70d4..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_help.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_home.png b/app/src/main/res/drawable-xxhdpi/ic_home.png deleted file mode 100644 index a576a1a4..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_home.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings.png b/app/src/main/res/drawable-xxhdpi/ic_settings.png deleted file mode 100644 index 2a50df97..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_settings.png and /dev/null differ diff --git a/app/src/main/res/drawable/app_icon.xml b/app/src/main/res/drawable/app_icon.xml new file mode 100644 index 00000000..70779250 --- /dev/null +++ b/app/src/main/res/drawable/app_icon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/arrow_right.xml b/app/src/main/res/drawable/arrow_right.xml deleted file mode 100644 index 0ac62543..00000000 --- a/app/src/main/res/drawable/arrow_right.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/cancel.xml b/app/src/main/res/drawable/cancel.xml deleted file mode 100644 index 147fbdfe..00000000 --- a/app/src/main/res/drawable/cancel.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/clock_end.xml b/app/src/main/res/drawable/clock_end.xml deleted file mode 100644 index 8f636ac6..00000000 --- a/app/src/main/res/drawable/clock_end.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/clock_start.xml b/app/src/main/res/drawable/clock_start.xml deleted file mode 100644 index f131c300..00000000 --- a/app/src/main/res/drawable/clock_start.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/github_circle.xml b/app/src/main/res/drawable/github_circle.xml deleted file mode 100644 index 297143ec..00000000 --- a/app/src/main/res/drawable/github_circle.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_jump.xml b/app/src/main/res/drawable/ic_jump.xml deleted file mode 100644 index 6232799e..00000000 --- a/app/src/main/res/drawable/ic_jump.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_message.xml b/app/src/main/res/drawable/ic_message.xml deleted file mode 100644 index 4e3135eb..00000000 --- a/app/src/main/res/drawable/ic_message.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_progressbar.xml b/app/src/main/res/drawable/ic_progressbar.xml deleted file mode 100644 index 7d0e371f..00000000 --- a/app/src/main/res/drawable/ic_progressbar.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_cache.xml b/app/src/main/res/drawable/ic_progressbar_cache.xml deleted file mode 100644 index ca0643ea..00000000 --- a/app/src/main/res/drawable/ic_progressbar_cache.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_complete.xml b/app/src/main/res/drawable/ic_progressbar_complete.xml deleted file mode 100644 index c23c3b6b..00000000 --- a/app/src/main/res/drawable/ic_progressbar_complete.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_progressbar_complete_cache.xml b/app/src/main/res/drawable/ic_progressbar_complete_cache.xml deleted file mode 100644 index 2fd4e05c..00000000 --- a/app/src/main/res/drawable/ic_progressbar_complete_cache.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon.xml b/app/src/main/res/drawable/icon.xml deleted file mode 100644 index 8bca8ba9..00000000 --- a/app/src/main/res/drawable/icon.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_red.xml b/app/src/main/res/drawable/icon_red.xml deleted file mode 100644 index 00fdcd21..00000000 --- a/app/src/main/res/drawable/icon_red.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml deleted file mode 100644 index e058761e..00000000 --- a/app/src/main/res/drawable/menu.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/shuffle_variant.xml b/app/src/main/res/drawable/shuffle_variant.xml deleted file mode 100644 index f954eb87..00000000 --- a/app/src/main/res/drawable/shuffle_variant.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/side_nav_bar.png b/app/src/main/res/drawable/side_nav_bar.png deleted file mode 100644 index 727e1e96..00000000 Binary files a/app/src/main/res/drawable/side_nav_bar.png and /dev/null differ diff --git a/app/src/main/res/drawable/sort_variant.xml b/app/src/main/res/drawable/sort_variant.xml deleted file mode 100644 index 83d5e4cb..00000000 --- a/app/src/main/res/drawable/sort_variant.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_drawable.xml b/app/src/main/res/drawable/thumb_drawable.xml deleted file mode 100644 index f72a3577..00000000 --- a/app/src/main/res/drawable/thumb_drawable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/track.xml b/app/src/main/res/drawable/track.xml deleted file mode 100644 index 770f3075..00000000 --- a/app/src/main/res/drawable/track.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/track_drawable.xml b/app/src/main/res/drawable/track_drawable.xml deleted file mode 100644 index 22113141..00000000 --- a/app/src/main/res/drawable/track_drawable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/gallery_dialog.xml b/app/src/main/res/layout/gallery_dialog.xml deleted file mode 100644 index cb7c017f..00000000 --- a/app/src/main/res/layout/gallery_dialog.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_details.xml b/app/src/main/res/layout/gallery_dialog_details.xml deleted file mode 100644 index 1c3448b7..00000000 --- a/app/src/main/res/layout/gallery_dialog_details.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_dotindicator.xml b/app/src/main/res/layout/gallery_dialog_dotindicator.xml deleted file mode 100644 index 02b91987..00000000 --- a/app/src/main/res/layout/gallery_dialog_dotindicator.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/gallery_dialog_tags.xml b/app/src/main/res/layout/gallery_dialog_tags.xml deleted file mode 100644 index 539d34a8..00000000 --- a/app/src/main/res/layout/gallery_dialog_tags.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/galleryblock_item.xml b/app/src/main/res/layout/galleryblock_item.xml deleted file mode 100644 index 7638f8e7..00000000 --- a/app/src/main/res/layout/galleryblock_item.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml deleted file mode 100644 index 3a1ebd5f..00000000 --- a/app/src/main/res/layout/main_activity.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity_content.xml b/app/src/main/res/layout/main_activity_content.xml deleted file mode 100644 index abbf1aca..00000000 --- a/app/src/main/res/layout/main_activity_content.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/mirrors_item.xml b/app/src/main/res/layout/mirrors_item.xml deleted file mode 100644 index 2b349771..00000000 --- a/app/src/main/res/layout/mirrors_item.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml deleted file mode 100644 index a413c18b..00000000 --- a/app/src/main/res/layout/nav_header_main.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/swipe_pageturn_view.xml b/app/src/main/res/layout/swipe_pageturn_view.xml deleted file mode 100644 index 6d1f82e2..00000000 --- a/app/src/main/res/layout/swipe_pageturn_view.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml deleted file mode 100644 index 2d45b181..00000000 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml deleted file mode 100644 index b9fd3fa8..00000000 --- a/app/src/main/res/menu/main.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ea44ad2f..26d6fbc1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,9 +21,9 @@ 履歴を削除 履歴を削除しますか? 履歴数: %1$d - 履歴 + 履歴 通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。 - トップ + トップ # リリースノート(v%1$s)\n%2$s セキュリティーモード アプリ履歴でアプリの画面を表示しない @@ -45,7 +45,7 @@ ダウンロード中… ダウンロード完了 バックグラウンドダウンロード中止 - ダウンロード + ダウンロード ページ移動 現ページ番号: %1$d\nページ数: %2$d hitomi.laに接続できません @@ -53,7 +53,7 @@ ダウンロード削除 ダウンロードしたギャラリーを全て削除します。\n実行しますか? ミラーサーバからイメージをロード - ブックマーク + ブックマーク ギャラリー番号で見る エラーが発生しました ストレージ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8554abb8..b242d065 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -20,9 +20,9 @@ 기록 삭제 기록을 삭제하시겠습니까? 기록 %1$d개 저장됨 - 기록 + 기록 백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다. - + # 릴리즈 노트(v%1$s)\n%2$s 최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다 보안 모드 활성화 @@ -44,14 +44,14 @@ 다운로드 중… 다운로드 완료 백그라운드 다운로드 취소 - 다운로드 + 다운로드 페이지 이동 현재 페이지: %1$d\n페이지 수: %2$d hitomi.la에 연결할 수 없습니다 %1$d 페이지로 이동 다운로드 삭제 다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까? - 즐겨찾기 + 즐겨찾기 갤러리 번호로 열기 갤러리를 찾지 못했습니다 저장 공간 diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 99cbc5ee..83638bef 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -1,15 +1,7 @@ - 16dp - 16dp - - 100dp - - 2000px - - 24dp + 24dp 72dp - 300dp \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 83022f36..112999b6 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -3,11 +3,8 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2cc2bbf..9a7720a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,10 +53,10 @@ Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled. - Home - History - Downloads - Favorites + Home + History + Downloads + Favorites Contact Help Visit homepage diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt index 27402b96..e2252de8 100644 --- a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -@file:Suppress("UNUSED_VARIABLE", "IncorrectScope") - package xyz.quaver.pupil /** @@ -26,14 +24,18 @@ package xyz.quaver.pupil * See [testing documentation](http://d.android.com/tools/testing). */ +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Test -import java.lang.reflect.ParameterizedType +import xyz.quaver.pupil.networking.HitomiHttpClient class ExampleUnitTest { @Test - fun test() { - val a = mutableSetOf() + fun test() = runTest { + val hitomi = HitomiHttpClient() - print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType) + val result = hitomi.getGalleryIDsFromNozomi(null, "index", "all") + + println(result.array()) } } diff --git a/build.gradle b/build.gradle index bd179094..f1fcd161 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" diff --git a/gradle.properties b/gradle.properties index f7137ba7..d5361bba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ kotlin.code.style=official android.enableJetifier=true android.useAndroidX=true -kotlin_version=1.9.0 \ No newline at end of file +kotlin_version=1.9.22 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cc366756..7044d5b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip