Migrated networking to ktor
@@ -14,27 +14,11 @@ if (file("google-services.json").exists()) {
|
|||||||
logger.lifecycle("Firebase Disabled")
|
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 {
|
android {
|
||||||
|
namespace 'xyz.quaver.pupil'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 21
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode 69
|
versionCode 69
|
||||||
@@ -44,8 +28,6 @@ android {
|
|||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
defaultConfig.minSdkVersion 21
|
|
||||||
|
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
|
|
||||||
@@ -65,33 +47,58 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
compose true
|
||||||
kotlinOptions {
|
buildConfig true
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
||||||
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
|
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
|
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||||
implementation "androidx.biometric:biometric:1.1.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"
|
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:BigImageViewer:1.8.1'
|
||||||
implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
|
implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
|
||||||
implementation 'com.github.piasy:FrescoImageViewFactory: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
|
//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"
|
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
|
||||||
|
|
||||||
@@ -133,9 +140,9 @@ dependencies {
|
|||||||
implementation "xyz.quaver:floatingsearchview:1.1.7"
|
implementation "xyz.quaver:floatingsearchview:1.1.7"
|
||||||
|
|
||||||
testImplementation "junit:junit:4.13.2"
|
testImplementation "junit:junit:4.13.2"
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"
|
||||||
androidTestImplementation "androidx.test.ext:junit:1.1.3"
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
androidTestImplementation "androidx.test:rules:1.4.0"
|
androidTestImplementation "androidx.test:rules:1.5.0"
|
||||||
androidTestImplementation "androidx.test:runner:1.4.0"
|
androidTestImplementation "androidx.test:runner:1.5.2"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||||
}
|
}
|
||||||
@@ -44,8 +44,6 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import xyz.quaver.io.FileX
|
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.types.Tag
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import java.io.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
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -110,7 +110,7 @@ fun URL.readText(settings: HeaderSetter? = null): String {
|
|||||||
settings?.invoke(it) ?: it
|
settings?.invoke(it) ?: it
|
||||||
}.build()
|
}.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 {
|
fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
|
||||||
@@ -119,7 +119,7 @@ fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
|
|||||||
settings?.invoke(it) ?: it
|
settings?.invoke(it) ?: it
|
||||||
}.build()
|
}.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")
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
@@ -161,8 +161,8 @@ object gg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
if (!call.isCanceled) {
|
if (!call.isCanceled()) {
|
||||||
response.body()?.use {
|
response.body?.use {
|
||||||
continuation.resume(it.string()) {
|
continuation.resume(it.string()) {
|
||||||
call.cancel()
|
call.cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ fun sanitize(input: String) : String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getIndexVersion(name: 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
|
//search.js
|
||||||
fun getGalleryIDsForQuery(query: String) : Set<Int> {
|
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)
|
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> {
|
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
|
val (offset, length) = data
|
||||||
if (length > 10000 || length <= 0)
|
if (length > 10000 || length <= 0)
|
||||||
throw Exception("length $length is too long")
|
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> {
|
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
|
||||||
val nozomiAddress =
|
val nozomiAddress =
|
||||||
when(area) {
|
when(area) {
|
||||||
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
|
null -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
|
||||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
else -> "$protocol//${xyz.quaver.pupil.networking.domain}/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
||||||
}
|
}
|
||||||
|
|
||||||
val bytes = try {
|
val bytes = try {
|
||||||
@@ -185,7 +185,7 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
|
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
|
val (offset, length) = data
|
||||||
if (length > 100000000 || length <= 0)
|
if (length > 100000000 || length <= 0)
|
||||||
throw Exception("length $length is too long")
|
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? {
|
fun getNodeAtAddress(field: String, address: Long) : Node? {
|
||||||
val url =
|
val url =
|
||||||
when(field) {
|
when(field) {
|
||||||
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
|
"galleries" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/galleries.$galleries_index_version.index"
|
||||||
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
|
"languages" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/languages.$galleries_index_version.index"
|
||||||
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
|
"nozomiurl" -> "$protocol//${xyz.quaver.pupil.networking.domain}/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
|
||||||
else -> "$protocol//$domain/$index_dir/$field.$tag_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))
|
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}")
|
.header("Range", "bytes=${range.first}-${range.last}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf()
|
return client.newCall(request).execute().body?.use { it.bytes() } ?: byteArrayOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/src/main/java/xyz/quaver/pupil/networking/Node.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
override fun source(): BufferedSource {
|
override fun source(): BufferedSource {
|
||||||
if (bufferedSource == null)
|
if (bufferedSource == null)
|
||||||
bufferedSource = Okio.buffer(source(responseBody.source()))
|
bufferedSource = source(responseBody.source()).buffer()
|
||||||
|
|
||||||
return bufferedSource!!
|
return bufferedSource!!
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ class DownloadService : Service() {
|
|||||||
var limit = 10
|
var limit = 10
|
||||||
|
|
||||||
while (response?.isSuccessful != true) {
|
while (response?.isSuccessful != true) {
|
||||||
if (response?.code() == 503) {
|
if (response?.code == 503) {
|
||||||
Thread.sleep(200)
|
Thread.sleep(200)
|
||||||
} else if (--limit < 0)
|
} else if (--limit < 0)
|
||||||
break
|
break
|
||||||
@@ -191,7 +191,7 @@ class DownloadService : Service() {
|
|||||||
response = chain.proceed(request)
|
response = chain.proceed(request)
|
||||||
|
|
||||||
response!!.newBuilder()
|
response!!.newBuilder()
|
||||||
.body(response.body()?.let {
|
.body(response.body?.let {
|
||||||
ProgressResponseBody(request.tag(), it, progressListener)
|
ProgressResponseBody(request.tag(), it, progressListener)
|
||||||
}).build()
|
}).build()
|
||||||
}
|
}
|
||||||
@@ -228,11 +228,11 @@ class DownloadService : Service() {
|
|||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
|
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
|
||||||
val (galleryID, index, startId) = call.request().tag() as 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 {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
runCatching {
|
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()
|
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
|
||||||
|
|
||||||
Cache.getInstance(this@DownloadService, galleryID)
|
Cache.getInstance(this@DownloadService, galleryID)
|
||||||
@@ -257,13 +257,13 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(startId: Int? = null) {
|
fun cancel(startId: Int? = null) {
|
||||||
client.dispatcher().queuedCalls().filter {
|
client.dispatcher.queuedCalls().filter {
|
||||||
it.request().tag() is Tag
|
it.request().tag() is Tag
|
||||||
}.forEach {
|
}.forEach {
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
client.dispatcher().runningCalls().filter {
|
client.dispatcher.runningCalls().filter {
|
||||||
it.request().tag() is Tag
|
it.request().tag() is Tag
|
||||||
}.forEach {
|
}.forEach {
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||||
@@ -278,13 +278,13 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(galleryID: Int, startId: Int? = null) {
|
fun cancel(galleryID: Int, startId: Int? = null) {
|
||||||
client.dispatcher().queuedCalls().filter {
|
client.dispatcher.queuedCalls().filter {
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||||
}.forEach {
|
}.forEach {
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
client.dispatcher().runningCalls().filter {
|
client.dispatcher.runningCalls().filter {
|
||||||
(it.request().tag() as? Tag)?.galleryID == galleryID
|
(it.request().tag() as? Tag)?.galleryID == galleryID
|
||||||
}.forEach {
|
}.forEach {
|
||||||
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
(it.request().tag() as? Tag)?.startId?.let { stopSelf(it) }
|
||||||
@@ -350,7 +350,7 @@ class DownloadService : Service() {
|
|||||||
val queued = mutableSetOf<Int>()
|
val queued = mutableSetOf<Int>()
|
||||||
|
|
||||||
if (priority) {
|
if (priority) {
|
||||||
client.dispatcher().queuedCalls().forEach {
|
client.dispatcher.queuedCalls().forEach {
|
||||||
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
|
val queuedID = (it.request().tag() as? Tag)?.galleryID ?: return@forEach
|
||||||
|
|
||||||
if (queued.add(queuedID))
|
if (queued.add(queuedID))
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -18,842 +18,36 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
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.os.Bundle
|
||||||
import android.text.InputType
|
import androidx.activity.compose.setContent
|
||||||
import android.text.util.Linkify
|
import androidx.activity.enableEdgeToEdge
|
||||||
import android.view.KeyEvent
|
import androidx.activity.viewModels
|
||||||
import android.view.MenuItem
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
import android.view.View
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import android.view.animation.DecelerateInterpolator
|
import com.google.accompanist.adaptive.calculateDisplayFeatures
|
||||||
import android.widget.EditText
|
import xyz.quaver.pupil.ui.composable.PupilApp
|
||||||
import android.widget.TextView
|
import xyz.quaver.pupil.ui.theme.AppTheme
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class MainActivity : BaseActivity() {
|
||||||
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = MainActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
if (intent.action == Intent.ACTION_VIEW) {
|
val viewModel: MainViewModel by viewModels()
|
||||||
intent.dataString?.let { url ->
|
|
||||||
restore(url,
|
setContent {
|
||||||
onFailure = {
|
AppTheme {
|
||||||
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
val windowSize = calculateWindowSizeClass(this)
|
||||||
}, onSuccess = {
|
val displayFeatures = calculateDisplayFeatures(this)
|
||||||
Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package xyz.quaver.pupil.ui.composable
|
||||||
|
|
||||||
|
enum class ContentType {
|
||||||
|
SINGLE_PANE, DUAL_PANE
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
92
app/src/main/java/xyz/quaver/pupil/ui/composable/MainApp.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package xyz.quaver.pupil.ui.composable
|
||||||
|
|
||||||
|
enum class NavigationContentPosition {
|
||||||
|
TOP, CENTER
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package xyz.quaver.pupil.ui.composable
|
||||||
|
|
||||||
|
enum class NavigationType {
|
||||||
|
NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
67
app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt
Normal 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)
|
||||||
90
app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -190,7 +190,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
|
|||||||
.header("Referer", "https://hitomi.la/")
|
.header("Referer", "https://hitomi.la/")
|
||||||
.build()
|
.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 {
|
}.getOrNull()?.let { thumbnail -> kotlin.runCatching {
|
||||||
cacheFolder.getChild(".thumbnail").also {
|
cacheFolder.getChild(".thumbnail").also {
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun isDownloading(galleryID: Int): Boolean {
|
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)
|
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
|
@Synchronized
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ data class ProxyInfo(
|
|||||||
Authenticator { _, response ->
|
Authenticator { _, response ->
|
||||||
val credential = Credentials.basic(username, password)
|
val credential = Credentials.basic(username, password)
|
||||||
|
|
||||||
response.request().newBuilder()
|
response.request.newBuilder()
|
||||||
.header("Proxy-Authorization", credential)
|
.header("Proxy-Authorization", credential)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
|
|||||||
translations = emptyMap()
|
translations = emptyMap()
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
|
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
|
||||||
Request.Builder()
|
Request.Builder()
|
||||||
.url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
|
.url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
|
||||||
.build()
|
.build()
|
||||||
).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
|
).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()
|
Request.Builder()
|
||||||
.url(filesURL)
|
.url(filesURL)
|
||||||
.build()
|
.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 {
|
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
|
||||||
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
|
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
|
|||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
kotlin.runCatching {
|
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) {
|
when (data) {
|
||||||
is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int })
|
is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int })
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 676 B |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 628 B |
|
Before Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 1000 B |
|
Before Width: | Height: | Size: 395 B |
|
Before Width: | Height: | Size: 914 B |
12
app/src/main/res/drawable/app_icon.xml
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
|
Before Width: | Height: | Size: 67 KiB |
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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" />
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -21,9 +21,9 @@
|
|||||||
<string name="settings_clear_history">履歴を削除</string>
|
<string name="settings_clear_history">履歴を削除</string>
|
||||||
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
||||||
<string name="settings_clear_history_summary">履歴数: %1$d</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="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="update_release_note"># リリースノート(v%1$s)\n%2$s</string>
|
||||||
<string name="settings_security_mode_title">セキュリティーモード</string>
|
<string name="settings_security_mode_title">セキュリティーモード</string>
|
||||||
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
|
<string name="settings_security_mode_summary">アプリ履歴でアプリの画面を表示しない</string>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<string name="reader_notification_text">ダウンロード中…</string>
|
<string name="reader_notification_text">ダウンロード中…</string>
|
||||||
<string name="reader_notification_complete">ダウンロード完了</string>
|
<string name="reader_notification_complete">ダウンロード完了</string>
|
||||||
<string name="reader_fab_download_cancel">バックグラウンドダウンロード中止</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_title">ページ移動</string>
|
||||||
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
||||||
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||||
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||||
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</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="main_open_gallery_by_id">ギャラリー番号で見る</string>
|
||||||
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
|
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
|
||||||
<string name="settings_storage">ストレージ</string>
|
<string name="settings_storage">ストレージ</string>
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
<string name="settings_clear_history">기록 삭제</string>
|
<string name="settings_clear_history">기록 삭제</string>
|
||||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</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="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="update_release_note"># 릴리즈 노트(v%1$s)\n%2$s</string>
|
||||||
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
<string name="settings_security_mode_summary">최근 앱 목록 창에서 앱 화면을 보이지 않게 합니다</string>
|
||||||
<string name="settings_security_mode_title">보안 모드 활성화</string>
|
<string name="settings_security_mode_title">보안 모드 활성화</string>
|
||||||
@@ -44,14 +44,14 @@
|
|||||||
<string name="reader_notification_text">다운로드 중…</string>
|
<string name="reader_notification_text">다운로드 중…</string>
|
||||||
<string name="reader_notification_complete">다운로드 완료</string>
|
<string name="reader_notification_complete">다운로드 완료</string>
|
||||||
<string name="reader_fab_download_cancel">백그라운드 다운로드 취소</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_title">페이지 이동</string>
|
||||||
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
||||||
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
||||||
<string name="main_move_to_page">%1$d 페이지로 이동</string>
|
<string name="main_move_to_page">%1$d 페이지로 이동</string>
|
||||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||||
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</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="main_open_gallery_by_id">갤러리 번호로 열기</string>
|
||||||
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
|
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
|
||||||
<string name="settings_storage">저장 공간</string>
|
<string name="settings_storage">저장 공간</string>
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
<dimen name="thumb_width">24dp</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_height">72dp</dimen>
|
<dimen name="thumb_height">72dp</dimen>
|
||||||
|
|
||||||
<dimen name="thumbnail_page_height">300dp</dimen>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,11 +3,8 @@
|
|||||||
<item name="item_click_support" type="id" />
|
<item name="item_click_support" type="id" />
|
||||||
|
|
||||||
<item name="notification_id_update" 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_id" type="id" />
|
||||||
<item name="downloader_notification_request" type="id" />
|
|
||||||
|
|
||||||
<item name="notification_download_cancel_action" type="id" />
|
<item name="notification_download_cancel_action" type="id" />
|
||||||
<item name="notification_import_cancel_action" type="id" />
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -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="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_destination_search">Home</string>
|
||||||
<string name="main_drawer_history">History</string>
|
<string name="main_destination_history">History</string>
|
||||||
<string name="main_drawer_downloads">Downloads</string>
|
<string name="main_destination_downloads">Downloads</string>
|
||||||
<string name="main_drawer_favorite">Favorites</string>
|
<string name="main_destination_favorites">Favorites</string>
|
||||||
<string name="main_drawer_group_contact_title">Contact</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_help">Help</string>
|
||||||
<string name="main_drawer_group_contact_homepage">Visit homepage</string>
|
<string name="main_drawer_group_contact_homepage">Visit homepage</string>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
|
|
||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,14 +24,18 @@ package xyz.quaver.pupil
|
|||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.lang.reflect.ParameterizedType
|
import xyz.quaver.pupil.networking.HitomiHttpClient
|
||||||
|
|
||||||
class ExampleUnitTest {
|
class ExampleUnitTest {
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() = runTest {
|
||||||
val a = mutableSetOf<Int>()
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ kotlin.code.style=official
|
|||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
kotlin_version=1.9.0
|
kotlin_version=1.9.22
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|||||||