Migrated networking to ktor

This commit is contained in:
tom5079
2024-02-19 23:19:29 -08:00
parent 114158cf73
commit 72b0fa78bb
93 changed files with 915 additions and 3841 deletions

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), 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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
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()
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>) : RecyclerView.Adapter<ThumbnailPageAdapter.ViewHolder>() {
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
}

View File

@@ -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()
}

View File

@@ -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<Int> {
@@ -115,7 +115,7 @@ fun getSuggestionsForQuery(query: String) : List<Suggestion> {
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
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<Long, Int>) : List<Suggesti
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
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<Long, Int>) : Set<Int> {
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<Long, Int>) : Set<Int> {
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)

View File

@@ -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<Int> {
val result = mutableSetOf<Int>()
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<Int> = 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<Int>()
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<Int>()
queries.forEach {
val queryResult = it.await()
result.addAll(queryResult)
}
result
}
null -> getGalleryIDsFromNozomi(null, "index", "all").toSet()
}
}

View File

@@ -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<Key>,
val datas: List<Data>,
val subNodeAddresses: List<Long>
) {
data class Key(
private val key: UByteArray
): Comparable<Key> {
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..<minSize) {
if (this.key[i] < other.key[i]) {
return -1
} else if(this.key[i] > 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<Node.Key>()
for (i in 0..<numberOfKeys) {
val keySize = buffer.int
val key = ByteArray(keySize)
buffer.get(key)
keys.add(Node.Key(key.toUByteArray()))
}
val numberOfDatas = buffer.int
val datas = mutableListOf<Data>()
for (i in 0..<numberOfDatas) {
val offset = buffer.long
val length = buffer.int
datas.add(Data(offset, length))
}
val numberOfSubNodeAddresses = B+1
val subNodeAddresses = mutableListOf<Long>()
for (i in 0..<numberOfSubNodeAddresses) {
val subNodeAddress = buffer.long
subNodeAddresses.add(subNodeAddress)
}
return Node(keys, datas, subNodeAddresses)
}
}
val isLeaf: Boolean = subNodeAddresses.all { it == 0L }
fun locateKey(target: Key): Pair<Boolean, Int> {
val index = keys.indexOfFirst { key -> key <= target }
if (index == -1) {
return Pair(false, keys.size)
}
return Pair(keys[index] == target, index)
}
}

View File

@@ -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>
): SearchQuery {
init {
if (queries.isEmpty()) {
error("queries cannot be empty")
}
}
}
data class Or(
val queries: List<SearchQuery>
): SearchQuery {
init {
if (queries.isEmpty()) {
error("queries cannot be empty")
}
}
}
data class Not(
val query: SearchQuery
): SearchQuery
}

View File

@@ -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<Int>()
if (priority) {
client.dispatcher().queuedCalls().forEach {
client.dispatcher.queuedCalls().forEach {
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
if (queued.add(queuedID))

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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

View File

@@ -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<Int>()
private var query = ""
set(value) {
field = value
with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
}
private var queryStack = mutableListOf<String>()
private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
private var galleryIDs: Deferred<List<Int>>? = 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<SearchSuggestion>
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<MenuView>(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)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
package xyz.quaver.pupil.ui.composable
enum class ContentType {
SINGLE_PANE, DUAL_PANE
}

View File

@@ -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
}

View File

@@ -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<DisplayFeature>,
uiState: MainUIState
) {
val navigationType: NavigationType
val contentType: ContentType
val foldingFeature: FoldingFeature? = displayFeatures.filterIsInstance<FoldingFeature>().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()
}
}
}

View File

@@ -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
),
)

View File

@@ -0,0 +1,5 @@
package xyz.quaver.pupil.ui.composable
enum class NavigationContentPosition {
TOP, CENTER
}

View File

@@ -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")
}
}
}

View File

@@ -0,0 +1,5 @@
package xyz.quaver.pupil.ui.composable
enum class NavigationType {
NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Int>()
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()
}
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Metadata>(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<Preference>("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<Preference>("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<Preference>("clear_history")) {
this ?: return@with
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = this@ManageStorageFragment
}
with(findPreference<Preference>("recover_downloads")) {
this ?: return@with
onPreferenceClickListener = this@ManageStorageFragment
}
}
override fun onDestroy() {
job?.cancel()
super.onDestroy()
}
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SearchInputView>(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<ImageView>(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<TextView>(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<ImageView>(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))
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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)
}
}

View File

@@ -1,100 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.view
import android.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)
}
}

View File

@@ -1,100 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.view
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.util.Log
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import kotlinx.coroutines.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.Tags
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
object Defaults {
const val maxChipSize = 10
}
var maxChipSize: Int = Defaults.maxChipSize
set(value) {
field = value
refresh()
}
private val moreView = Chip(context).apply {
text = ""
setEnsureMinTouchTargetSize(false)
setOnClickListener {
removeView(this)
for (i in maxChipSize until tags.size) {
val tag = tags.elementAt(i)
addView(TagChip(context, 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()
}
}

View File

@@ -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
)

View File

@@ -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())

View File

@@ -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

View File

@@ -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()
}

View File

@@ -44,10 +44,10 @@ fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
translations = emptyMap()
kotlin.runCatching {
translations = Json.decodeFromString<Map<String, String>>(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<String> {
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 != '.' }

View File

@@ -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 })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 B

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="900dp"
android:height="900dp"
android:viewportWidth="900"
android:viewportHeight="900">
<path
android:pathData="M450,450m-450,0a450,450 0,1 1,900 0a450,450 0,1 1,-900 0"
android:fillColor="#4EC1F5"/>
<path
android:pathData="M450,450m-175,0a175,175 0,1 1,350 0a175,175 0,1 1,-350 0"
android:fillColor="#1D1D1D"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/arrow_right.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11H4z"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/cancel.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8c0 1.85 0.63 3.55 1.68 4.91L16.91 5.68C15.55 4.63 13.85 4 12 4m0 16a8 8 0 0 0 8-8c0-1.85-0.63-3.55-1.68-4.91L7.09 18.32C8.45 19.37 10.15 20 12 20z"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/clock_end.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M15 16v3H3v2h12v3l4-4m0 0v4h2v-8h-2"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/clock_start.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M12 1C8.14 1 5 4.14 5 8a7 7 0 0 0 7 7c3.86 0 7-3.13 7-7 0-3.86-3.14-7-7-7m0 2.15c2.67 0 4.85 2.17 4.85 4.85 0 2.68-2.18 4.85-4.85 4.85A4.85 4.85 0 0 1 7.15 8 4.85 4.85 0 0 1 12 3.15M11 5v3.69l3.19 1.84 0.75-1.3-2.44-1.41V5M4 16v8h2v-3h12v3l4-4-4-4v3H6v-3"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/github-circle.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5 0.5 0.08 0.66-0.23 0.66-0.5v-1.69c-2.77 0.6-3.36-1.34-3.36-1.34-0.46-1.16-1.11-1.47-1.11-1.47-0.91-0.62 0.07-0.6 0.07-0.6 1 0.07 1.53 1.03 1.53 1.03 0.87 1.52 2.34 1.07 2.91 0.83 0.09-0.65 0.35-1.09 0.63-1.34-2.22-0.25-4.55-1.11-4.55-4.92 0-1.11 0.38-2 1.03-2.71-0.1-0.25-0.45-1.29 0.1-2.64 0 0 0.84-0.27 2.75 1.02 0.79-0.22 1.65-0.33 2.5-0.33 0.85 0 1.71 0.11 2.5 0.33 1.91-1.29 2.75-1.02 2.75-1.02 0.55 1.35 0.2 2.39 0.1 2.64 0.65 0.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91 0.36 0.31 0.69 0.92 0.69 1.85V21c0 0.27 0.16 0.59 0.67 0.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"/>
</vector>

View File

@@ -1,4 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#fff" android:pathData="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73 0.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/>
<path android:fillColor="#fff" android:pathData="M8.5 15a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 8.5 15z"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/message.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4c0-1.11-0.9-2-2-2z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tintMode="multiply">
<path android:name="path_1" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#b9f6ca" android:strokeWidth="24"/>
<path android:name="path" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#00c853" android:strokeWidth="24"/>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tintMode="multiply">
<path android:name="path_1" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#80d8ff" android:strokeWidth="24"/>
<path android:name="path" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#0091ea" android:strokeWidth="24"/>
</vector>

View File

@@ -1,13 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:aapt="http://schemas.android.com/aapt" tools:ignore="NewApi">
<aapt:attr name="android:drawable">
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tintMode="multiply">
<path android:name="path_1" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#b9f6ca" android:strokeWidth="24"/>
<path android:name="path" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#00c853" android:strokeWidth="24"/>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator android:propertyName="trimPathEnd" android:duration="1000" android:valueFrom="0" android:valueTo="1" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,13 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:aapt="http://schemas.android.com/aapt" tools:ignore="NewApi">
<aapt:attr name="android:drawable">
<vector android:name="vector" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tintMode="multiply">
<path android:name="path_1" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#80d8ff" android:strokeWidth="24"/>
<path android:name="path" android:pathData="M 0 12 L 24 12" android:fillColor="#000" android:strokeColor="#0091ea" android:strokeWidth="24"/>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator android:propertyName="trimPathEnd" android:duration="1000" android:valueFrom="0" android:valueTo="1" android:valueType="floatType" android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,30 +0,0 @@
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="640">
<path
android:pathData="M640,320C640,496.61 496.61,640 320,640C143.39,640 0,496.61 0,320C0,143.38 143.39,0 320,0C496.61,0 640,143.38 640,320Z"
android:fillColor="#4ec1f5"/>
<path
android:pathData="M420,320C420,375.19 375.19,420 320,420C264.81,420 220,375.19 220,320C220,264.81 264.81,220 320,220C375.19,220 420,264.81 420,320Z"
android:fillColor="#1d1d1d"/>
</vector>

View File

@@ -1,30 +0,0 @@
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="640">
<path
android:pathData="M640,320C640,496.61 496.61,640 320,640C143.39,640 0,496.61 0,320C0,143.38 143.39,0 320,0C496.61,0 640,143.38 640,320Z"
android:fillColor="@color/colorAccent"/>
<path
android:pathData="M420,320C420,375.19 375.19,420 320,420C264.81,420 220,375.19 220,320C220,264.81 264.81,220 320,220C375.19,220 420,264.81 420,320Z"
android:fillColor="#1d1d1d"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/menu.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="?attr/colorControlNormal" android:pathData="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2z"/>
</vector>

View File

@@ -1,4 +0,0 @@
<!--drawable/shuffle_variant.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M17 3l5.25 4.5L17 12l5.25 4.5L17 21v-3h-2.74l-2.82-2.82 2.12-2.12L15.5 15H17V9h-1.5l-9 9H2v-3h3.26l9-9H17V3M2 6h4.5l2.82 2.82-2.12 2.12L5.26 9H2V6z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,8 +0,0 @@
<!-- drawable/sort_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Pupil, Hitomi.la viewer for Android ~ Copyright (C) 2020 tom5079 ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by ~ the Free Software Foundation, either version 3 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU General Public License for more details. ~ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see <http://www.gnu.org/licenses/>.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/thumb"/>
<item android:drawable="@drawable/thumb"/>
</selector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Pupil, Hitomi.la viewer for Android ~ Copyright (C) 2020 tom5079 ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by ~ the Free Software Foundation, either version 3 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU General Public License for more details. ~ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see <http://www.gnu.org/licenses/>.-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
<padding android:top="10dp" android:left="10dp" android:right="10dp" android:bottom="10dp"/>
</shape>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Pupil, Hitomi.la viewer for Android ~ Copyright (C) 2020 tom5079 ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by ~ the Free Software Foundation, either version 3 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU General Public License for more details. ~ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see <http://www.gnu.org/licenses/>.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/track"/>
<item android:drawable="@drawable/track"/>
</selector>

View File

@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<com.github.piasy.biv.view.BigImageView
android:id="@+id/cover"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/title"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintLeft_toRightOf="@id/cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/artist"
app:layout_constraintBottom_toTopOf="@id/type"/>
<com.google.android.material.chip.Chip
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/cover"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_anchor="@id/toolbar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"/>
<LinearLayout
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator
android:id="@+id/dotindicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
app:dotsSize="8dp"/>
</RelativeLayout>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
style="@style/TextAppearance.MaterialComponents.Body2"
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="8dp"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingVertical="4dp"/>
</LinearLayout>

View File

@@ -1,160 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<xyz.quaver.pupil.ui.view.ProgressCard
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/galleryblock_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="true"
app:cardCornerRadius="4dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:cardUseCompatPadding="true"
tools:ignore="RtlHardcoded">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.github.piasy.biv.view.BigImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="0dp"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
android:clickable="false"
app:layout_constraintHeight_default="spread"
app:layout_constraintHeight_min="200dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title" />
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<xyz.quaver.pupil.ui.view.TagChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="galleryblock_thumbnail, galleryblock_tag_group"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@id/barrier"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/galleryblock_pagecount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:srcCompat="@drawable/ic_star_empty"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</xyz.quaver.pupil.ui.view.ProgressCard>

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start">
<include android:id="@+id/contents"
layout="@layout/main_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -1,127 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<xyz.quaver.pupil.ui.view.MainView
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView
android:id="@+id/noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/main_no_result"
android:linksClickable="true"
android:visibility="invisible"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/cancel_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/jump_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/random_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/id_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
<xyz.quaver.pupil.ui.view.FloatingSearchView
android:id="@+id/searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:searchBarMarginLeft="6dp"
app:searchBarMarginRight="6dp"
app:searchBarMarginTop="6dp"
app:searchHint="@string/search_hint"
app:suggestionAnimDuration="250"
app:showSearchKey="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:dismissOnOutsideTouch="true"
app:close_search_on_keyboard_dismiss="false" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingStart="32dp"
android:paddingLeft="32dp"
android:paddingEnd="32dp"
android:paddingRight="32dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/mirror_name"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/mirror_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/menu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/side_nav_bar"
android:adjustViewBounds="true"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="bottom"
tools:ignore="ContentDescription" />

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.FrameLayout">
<TextView
android:id="@+id/prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center"
android:maxLines="1"
android:ellipsize="end"
app:drawableStartCompat="@drawable/navigate_prev"
app:drawableLeftCompat="@drawable/navigate_prev"
app:drawableTint="@color/colorAccent" />
<TextView
android:id="@+id/next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:maxLines="1"
android:ellipsize="end"
app:drawableEndCompat="@drawable/navigate_next"
app:drawableRightCompat="@drawable/navigate_next"
app:drawableTint="@color/colorAccent" />
</merge>

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item android:id="@+id/main_drawer_home"
android:title="@string/main_drawer_home"
android:checked="true"
android:icon="@drawable/ic_home"/>
<item android:id="@+id/main_drawer_history"
android:title="@string/main_drawer_history"
android:icon="@drawable/history"/>
<item android:id="@+id/main_drawer_downloads"
android:title="@string/main_drawer_downloads"
android:icon="@drawable/ic_download"/>
<item android:id="@+id/main_drawer_favorite"
android:title="@string/main_drawer_favorite"
android:icon="@drawable/ic_star_filled"/>
</group>
<item android:title="@string/main_drawer_group_contact_title">
<menu>
<item android:id="@+id/main_drawer_help"
android:title="@string/main_drawer_group_contact_help"
android:icon="@drawable/ic_help"/>
<item android:id="@+id/main_drawer_github"
android:title="@string/main_drawer_group_contact_github"
android:icon="@drawable/github_circle"/>
<item android:id="@+id/main_drawer_homepage"
android:title="@string/main_drawer_group_contact_homepage"
android:icon="@drawable/ic_home"/>
<item android:id="@+id/main_drawer_email"
android:title="@string/main_drawer_group_contact_email"
android:icon="@drawable/ic_email"/>
<item android:id="@+id/main_drawer_kakaotalk"
android:title="@string/main_drawer_grouop_contact_discord"
android:icon="@drawable/ic_message"/>
</menu>
</item>
</menu>

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/sort"
android:title="@string/main_menu_sort"
android:icon="@drawable/sort_variant"
app:showAsAction="ifRoom">
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest"
android:title="@string/main_menu_sort_newest"
android:checked="true"/>
<item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
</group>
</menu>
</item>
<item
android:id="@+id/main_menu_settings"
android:icon="@drawable/ic_settings"
android:title="@string/main_settings"
app:showAsAction="always"/>
<item android:id="@+id/main_menu_thin"
android:title="@string/main_menu_thin"
app:showAsAction="never"
android:checkable="true"/>
</menu>

View File

@@ -21,9 +21,9 @@
<string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string>
<string name="main_drawer_history">履歴</string>
<string name="main_destination_history">履歴</string>
<string name="notification_denied">通知を無効にするとバックグラウンドダウンロード及びアプリのアップデート機能が使用不可になります。</string>
<string name="main_drawer_home">トップ</string>
<string name="main_destination_search">トップ</string>
<string name="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
<string name="settings_security_mode_title">セキュリティーモード</string>
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
@@ -45,7 +45,7 @@
<string name="reader_notification_text">ダウンロード中…</string>
<string name="reader_notification_complete">ダウンロード完了</string>
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</string>
<string name="main_drawer_downloads">ダウンロード</string>
<string name="main_destination_downloads">ダウンロード</string>
<string name="main_jump_title">ページ移動</string>
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
<string name="unable_to_connect">hitomi.laに接続できません</string>
@@ -53,7 +53,7 @@
<string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
<string name="main_drawer_favorite">ブックマーク</string>
<string name="main_destination_favorites">ブックマーク</string>
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string>

View File

@@ -20,9 +20,9 @@
<string name="settings_clear_history">기록 삭제</string>
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
<string name="main_drawer_history">기록</string>
<string name="main_destination_history">기록</string>
<string name="notification_denied">백그라운드 다운로드를 위해서는 알림을 활성화할 필요가 있습니다. 알림을 비활성화하면 백그라운드 다운로드와 앱 업데이트 기능을 사용할 수 없습니다.</string>
<string name="main_drawer_home"></string>
<string name="main_destination_search"></string>
<string name="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
<string name="settings_security_mode_title">보안 모드 활성화</string>
@@ -44,14 +44,14 @@
<string name="reader_notification_text">다운로드 중…</string>
<string name="reader_notification_complete">다운로드 완료</string>
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</string>
<string name="main_drawer_downloads">다운로드</string>
<string name="main_destination_downloads">다운로드</string>
<string name="main_jump_title">페이지 이동</string>
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
<string name="main_move_to_page">%1$d 페이지로 이동</string>
<string name="settings_clear_downloads">다운로드 삭제</string>
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
<string name="main_drawer_favorite">즐겨찾기</string>
<string name="main_destination_favorites">즐겨찾기</string>
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
<string name="settings_storage">저장 공간</string>

View File

@@ -1,15 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="galleryblock_thumbnail_thin">100dp</dimen>
<dimen name="reader_max_height" tools:ignore="PxUsage">2000px</dimen>
<dimen name="thumb_width">24dp</dimen>
<dimen name="thumb_width">24dp</dimen>
<dimen name="thumb_height">72dp</dimen>
<dimen name="thumbnail_page_height">300dp</dimen>
</resources>

View File

@@ -3,11 +3,8 @@
<item name="item_click_support" type="id" />
<item name="notification_id_update" type="id" />
<item name="notification_id_import" type="id" />
<item name="downloader_notification_id" type="id" />
<item name="downloader_notification_request" type="id" />
<item name="notification_download_cancel_action" type="id" />
<item name="notification_import_cancel_action" type="id" />
</resources>

View File

@@ -53,10 +53,10 @@
<string name="notification_denied">Notification permission is required for background downloads. If you deny notifications from this app, in-app update and background download will be disabled.</string>
<string name="main_drawer_home">Home</string>
<string name="main_drawer_history">History</string>
<string name="main_drawer_downloads">Downloads</string>
<string name="main_drawer_favorite">Favorites</string>
<string name="main_destination_search">Home</string>
<string name="main_destination_history">History</string>
<string name="main_destination_downloads">Downloads</string>
<string name="main_destination_favorites">Favorites</string>
<string name="main_drawer_group_contact_title">Contact</string>
<string name="main_drawer_group_contact_help">Help</string>
<string name="main_drawer_group_contact_homepage">Visit homepage</string>

View File

@@ -16,8 +16,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@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<Int>()
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())
}
}

View File

@@ -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"

View File

@@ -20,4 +20,4 @@ kotlin.code.style=official
android.enableJetifier=true
android.useAndroidX=true
kotlin_version=1.9.0
kotlin_version=1.9.22

View File

@@ -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