WIP
This commit is contained in:
@@ -44,23 +44,12 @@ import okhttp3.*
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.android.x.androidXModule
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.sources.initSources
|
||||
import xyz.quaver.pupil.sources.sourceModule
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import xyz.quaver.setClient
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
lateinit var histories: SavedSet<String>
|
||||
private set
|
||||
lateinit var favorites: SavedSet<String>
|
||||
private set
|
||||
lateinit var favoriteTags: SavedSet<Tag>
|
||||
private set
|
||||
lateinit var searchHistory: SavedSet<String>
|
||||
private set
|
||||
|
||||
lateinit var clientBuilder: OkHttpClient.Builder
|
||||
|
||||
var clientHolder: OkHttpClient? = null
|
||||
@@ -79,6 +68,11 @@ class Pupil : Application(), DIAware {
|
||||
bind<OkHttpClient>() with provider { client }
|
||||
bind<ImageCache>() with singleton { ImageCache(this@Pupil) }
|
||||
bind<DownloadManager>() with singleton { DownloadManager(this@Pupil) }
|
||||
|
||||
bind<SavedSourceSet>(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "histories.json")) }
|
||||
bind<SavedSourceSet>(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favorites.json")) }
|
||||
bind<SavedSourceSet>(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favoriteTags.json")) }
|
||||
bind<SavedSourceSet>(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "searchHistory.json")) }
|
||||
}
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
@@ -93,8 +87,6 @@ class Pupil : Application(), DIAware {
|
||||
else userID
|
||||
}
|
||||
|
||||
initSources(this)
|
||||
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
FirebaseCrashlytics.getInstance().setUserId(userID)
|
||||
|
||||
@@ -125,11 +117,6 @@ class Pupil : Application(), DIAware {
|
||||
Preferences["reset_secure"] = true
|
||||
}
|
||||
|
||||
histories = SavedSet(File(ContextCompat.getDataDir(this), "histories.json"), "")
|
||||
favorites = SavedSet(File(ContextCompat.getDataDir(this), "favorites.json"), "")
|
||||
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
|
||||
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)
|
||||
|
||||
|
||||
@@ -60,8 +60,12 @@ class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderIt
|
||||
with (binding.image) {
|
||||
setImageViewFactory(FrescoImageViewFactory().apply {
|
||||
updateView = { imageInfo ->
|
||||
layoutParams.height = imageInfo.height
|
||||
(mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat()
|
||||
if (!fullscreen) {
|
||||
binding.root.layoutParams.height = imageInfo.height
|
||||
layoutParams.height = imageInfo.height
|
||||
|
||||
(mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat()
|
||||
}
|
||||
}
|
||||
})
|
||||
setImageShownCallback(object: ImageShownCallback {
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.daimajia.swipe.SwipeLayout
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
@@ -35,13 +36,15 @@ import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.imagepipeline.image.ImageInfo
|
||||
import kotlinx.coroutines.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.SearchResultItemBinding
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
||||
class SearchResultsAdapter(var results: LiveData<List<ItemInfo>>) : RecyclerSwipeAdapter<SearchResultsAdapter.ViewHolder>(), SwipeAdapterInterface {
|
||||
|
||||
var onChipClickedHandler: ((Tag) -> Unit)? = null
|
||||
var onDownloadClickedHandler: ((source: String, itemI: String) -> Unit)? = null
|
||||
@@ -64,6 +67,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
||||
}
|
||||
|
||||
binding.idView.setOnClickListener {
|
||||
// TODO: MEMLEAK
|
||||
(itemView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("item_id", itemID)
|
||||
)
|
||||
@@ -146,6 +150,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
with (binding.tagGroup) {
|
||||
tags.clear()
|
||||
source = result.source
|
||||
result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.let { if (it.size == 1 && it.first().isEmpty()) emptyList() else it }?.map {
|
||||
Tag.parse(it)
|
||||
}?.let { tags.addAll(it) }
|
||||
@@ -201,10 +206,10 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
|
||||
@ExperimentalTime
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
mItemManger.bindView(holder.itemView, position)
|
||||
holder.bind(results[position])
|
||||
holder.bind(results.value!![position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = results.size
|
||||
override fun getItemCount(): Int = results.value?.size ?: 0
|
||||
|
||||
override fun getSwipeLayoutResourceId(position: Int): Int = R.id.swipe_layout
|
||||
|
||||
|
||||
@@ -23,26 +23,30 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.sourceIcons
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
|
||||
class SourceAdapter(private val sources: List<Source<*, SearchSuggestion>>) : RecyclerView.Adapter<SourceAdapter.ViewHolder>() {
|
||||
class SourceAdapter(sources: SourceEntries) : RecyclerView.Adapter<SourceAdapter.ViewHolder>() {
|
||||
|
||||
var onSourceSelectedListener: ((Source<*, SearchSuggestion>) -> Unit)? = null
|
||||
var onSourceSelectedListener: ((String) -> Unit)? = null
|
||||
|
||||
private val sources = sources.toList()
|
||||
|
||||
inner class ViewHolder(private val binding: SourceSelectDialogItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var source: Source<*, SearchSuggestion>
|
||||
lateinit var source: AnySource
|
||||
|
||||
init {
|
||||
binding.go.setOnClickListener {
|
||||
onSourceSelectedListener?.invoke(source)
|
||||
onSourceSelectedListener?.invoke(source.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(source: Source<*, SearchSuggestion>) {
|
||||
fun bind(source: AnySource) {
|
||||
this.source = source
|
||||
|
||||
binding.icon.setImageDrawable(sourceIcons[source.name])
|
||||
// TODO: save image somewhere else
|
||||
binding.icon.setImageResource(source.iconResID)
|
||||
binding.name.text = source.name
|
||||
}
|
||||
}
|
||||
@@ -52,7 +56,7 @@ class SourceAdapter(private val sources: List<Source<*, SearchSuggestion>>) : Re
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(sources[position])
|
||||
holder.bind(sources[position].second)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = sources.size
|
||||
|
||||
27
app/src/main/java/xyz/quaver/pupil/migrate/Migrate.kt
Normal file
27
app/src/main/java/xyz/quaver/pupil/migrate/Migrate.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 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.migrate
|
||||
|
||||
class Migrate {
|
||||
|
||||
fun migrate() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
25
app/src/main/java/xyz/quaver/pupil/migrate/Migrate001.kt
Normal file
25
app/src/main/java/xyz/quaver/pupil/migrate/Migrate001.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 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.migrate
|
||||
|
||||
class Migrate001 {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -29,10 +29,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.contexted
|
||||
import org.kodein.di.instance
|
||||
import org.kodein.di.*
|
||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.R
|
||||
@@ -139,29 +136,18 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
val sources = mutableMapOf<String, AnySource>()
|
||||
val sourceIcons = mutableMapOf<String, Drawable?>()
|
||||
|
||||
typealias SourceEntry = Pair<String, AnySource>
|
||||
typealias SourceEntries = Set<SourceEntry>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val sourceModule = DI.Module(name = "source") {
|
||||
listOf(
|
||||
Hitomi(),
|
||||
Hiyobi()
|
||||
).forEach {
|
||||
bind<AnySource>(tag = it.name) with instance (it as AnySource)
|
||||
}
|
||||
}
|
||||
bind() from setBinding<SourceEntry>()
|
||||
|
||||
@Deprecated("")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun initSources(context: Context) {
|
||||
// Add Default Sources
|
||||
listOf(
|
||||
Hitomi(),
|
||||
Hiyobi()
|
||||
).forEach {
|
||||
sources[it.name] = it as AnySource
|
||||
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
|
||||
).forEach { source ->
|
||||
bind<SourceEntry>().inSet() with multiton { _: Unit -> source.name to (source as AnySource) }
|
||||
}
|
||||
|
||||
bind<History>() with factory { source: String -> History(di, source) }
|
||||
}
|
||||
72
app/src/main/java/xyz/quaver/pupil/sources/History.kt
Normal file
72
app/src/main/java/xyz/quaver/pupil/sources/History.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 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.sources
|
||||
|
||||
import android.util.Log
|
||||
import com.orhanobut.logger.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.util.SavedSourceSet
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
class History(override val di: DI, source: String) : Source<DefaultSortMode, SearchSuggestion>(), DIAware {
|
||||
|
||||
private val source: AnySource by source(source)
|
||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
||||
|
||||
override val name: String
|
||||
get() = source.name
|
||||
override val iconResID: Int
|
||||
get() = source.iconResID
|
||||
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
|
||||
|
||||
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> {
|
||||
val channel = Channel<ItemInfo>()
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
Logger.d(histories.map)
|
||||
histories.map[source.name]?.forEach {
|
||||
channel.send(source.info(it))
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(channel, histories.map.size)
|
||||
}
|
||||
|
||||
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
||||
return source.suggestion(query)
|
||||
}
|
||||
|
||||
override suspend fun images(itemID: String): List<String> {
|
||||
return source.images(itemID)
|
||||
}
|
||||
|
||||
override suspend fun info(itemID: String): ItemInfo {
|
||||
return source.info(itemID)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
||||
}
|
||||
|
||||
override suspend fun images(itemID: String): List<String> {
|
||||
return createImgList(itemID, getGalleryInfo(itemID), false).map {
|
||||
return createImgList(itemID, getGalleryInfo(itemID), true).map {
|
||||
it.path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +70,12 @@ class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags
|
||||
companion object {
|
||||
fun parse(tags: String) : Tags {
|
||||
return Tags(
|
||||
tags.split(' ').map {
|
||||
tags.split(' ').mapNotNull {
|
||||
if (it.isNotEmpty())
|
||||
Tag.parse(it)
|
||||
else
|
||||
null
|
||||
}.filterNotNull().toMutableSet()
|
||||
}.toMutableSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ import android.text.util.Linkify
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -42,53 +42,38 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.orhanobut.logger.Logger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
import xyz.quaver.floatingsearchview.FloatingSearchView
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.pupil.*
|
||||
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
||||
import xyz.quaver.pupil.databinding.MainActivityBinding
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.sourceIcons
|
||||
import xyz.quaver.pupil.sources.sources
|
||||
import xyz.quaver.pupil.types.*
|
||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
|
||||
import xyz.quaver.pupil.ui.dialog.GalleryDialogFragment
|
||||
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
|
||||
import xyz.quaver.pupil.ui.view.ProgressCardView
|
||||
import xyz.quaver.pupil.ui.view.SwipePageTurnView
|
||||
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
class MainActivity :
|
||||
BaseActivity(),
|
||||
NavigationView.OnNavigationItemSelectedListener
|
||||
NavigationView.OnNavigationItemSelectedListener,
|
||||
DIAware
|
||||
{
|
||||
private val searchResults = mutableListOf<ItemInfo>()
|
||||
override val di by di()
|
||||
|
||||
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 val searchResults = mutableListOf<ItemInfo>()
|
||||
private var queryStack = mutableListOf<String>()
|
||||
|
||||
private lateinit var source: Source<*, SearchSuggestion>
|
||||
private lateinit var sortMode: Enum<*>
|
||||
|
||||
private var searchJob: Deferred<Pair<Channel<ItemInfo>, Int>>? = null
|
||||
private var totalItems = 0
|
||||
private var currentPage = 1
|
||||
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private val model: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -97,7 +82,7 @@ class MainActivity :
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
intent.dataString?.let { url ->
|
||||
restore(url,
|
||||
restore(this, url,
|
||||
onFailure = {
|
||||
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
|
||||
}, onSuccess = {
|
||||
@@ -113,6 +98,74 @@ class MainActivity :
|
||||
checkUpdate(this)
|
||||
|
||||
initView()
|
||||
|
||||
model.query.observe(this) {
|
||||
binding.contents.searchview.binding.querySection.searchBarText.run {
|
||||
if (text?.toString() != it) setText(it, TextView.BufferType.EDITABLE)
|
||||
}
|
||||
}
|
||||
|
||||
model.availableSortMode.observe(this) {
|
||||
binding.contents.searchview.post {
|
||||
binding.contents.searchview.binding.querySection.menuView.menuItems.findMenu(R.id.sort).subMenu.apply {
|
||||
clear()
|
||||
|
||||
it.forEach {
|
||||
add(R.id.sort_mode_group_id, it.ordinal, Menu.NONE, it.name)
|
||||
}
|
||||
|
||||
setGroupCheckable(R.id.sort_mode_group_id, true, true)
|
||||
|
||||
children.first().isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.sourceIcon.observe(this) {
|
||||
binding.contents.searchview.post {
|
||||
(binding.contents.searchview.binding.querySection.menuView.getChildAt(1) as ImageView).apply {
|
||||
ImageViewCompat.setImageTintList(this, null)
|
||||
|
||||
setImageResource(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.searchResults.observe(this) {
|
||||
binding.contents.recyclerview.post {
|
||||
if (model.loading) {
|
||||
if (it.isEmpty()) {
|
||||
binding.contents.noresult.hide()
|
||||
binding.contents.progressbar.show()
|
||||
|
||||
(binding.contents.recyclerview.adapter as RecyclerSwipeAdapter).run {
|
||||
mItemManger.closeAllItems()
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
ViewCompat.animate(binding.contents.searchview)
|
||||
.setDuration(100)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.translationY(0F)
|
||||
}
|
||||
} else {
|
||||
binding.contents.progressbar.hide()
|
||||
if (it.isEmpty()) {
|
||||
binding.contents.noresult.show()
|
||||
} else {
|
||||
binding.contents.recyclerview.adapter?.notifyItemInserted(it.size-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model.suggestions.observe(this) { runOnUiThread {
|
||||
Logger.d(it)
|
||||
binding.contents.searchview.swapSuggestions(
|
||||
if (it.isEmpty()) listOf(NoResultSuggestion(getString(R.string.main_no_result))) else it
|
||||
)
|
||||
} }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -126,36 +179,31 @@ class MainActivity :
|
||||
when {
|
||||
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
|
||||
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
|
||||
query = queryStack.last()
|
||||
model.query.value = queryStack.last()
|
||||
|
||||
query()
|
||||
model.query()
|
||||
}
|
||||
else -> super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
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 > 1) {
|
||||
if (model.currentPage.value!! > 1) {
|
||||
runOnUiThread {
|
||||
currentPage--
|
||||
|
||||
query()
|
||||
model.prevPage()
|
||||
model.query()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (currentPage < maxPage) {
|
||||
if (model.currentPage.value!! < model.totalPages.value!!) {
|
||||
runOnUiThread {
|
||||
currentPage++
|
||||
|
||||
query()
|
||||
model.nextPage()
|
||||
model.query()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,34 +213,6 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSource(source: Source<*, SearchSuggestion>) {
|
||||
this.source = source
|
||||
sortMode = source.availableSortMode.first()
|
||||
|
||||
query = ""
|
||||
currentPage = 1
|
||||
|
||||
with (binding.contents.searchview.binding.querySection.menuView) {
|
||||
post {
|
||||
menuItems.findMenu(R.id.sort).subMenu.apply {
|
||||
clear()
|
||||
|
||||
source.availableSortMode.forEach {
|
||||
add(R.id.sort_mode_group_id, it.ordinal, Menu.NONE, it.name)
|
||||
}
|
||||
|
||||
setGroupCheckable(R.id.sort_mode_group_id, true, true)
|
||||
|
||||
children.first().isChecked = true
|
||||
}
|
||||
with (getChildAt(1) as ImageView) {
|
||||
ImageViewCompat.setImageTintList(this, null)
|
||||
setImageDrawable(sourceIcons[source.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
@@ -234,14 +254,13 @@ class MainActivity :
|
||||
setTitle(R.string.main_jump_title)
|
||||
setMessage(getString(
|
||||
R.string.main_jump_message,
|
||||
currentPage,
|
||||
ceil(totalItems / perPage.toDouble()).roundToInt()
|
||||
model.currentPage.value!!,
|
||||
ceil(model.totalPages.value!! / perPage.toDouble()).roundToInt()
|
||||
))
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
|
||||
|
||||
query()
|
||||
model.setPage(editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
|
||||
model.query()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
@@ -251,30 +270,16 @@ class MainActivity :
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
setOnClickListener {
|
||||
setImageDrawable(CircularProgressDrawable(context))
|
||||
if (totalItems > 0)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val random = Random.Default.nextInt(totalItems)
|
||||
|
||||
val randomResult =
|
||||
source.search(
|
||||
query + Preferences["default_query", ""],
|
||||
random .. random,
|
||||
sortMode
|
||||
).first.receive()
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
GalleryDialogFragment(source.name, randomResult.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
||||
model.random { runOnUiThread {
|
||||
setImageResource(R.drawable.shuffle_variant)
|
||||
GalleryDialogFragment(model.sourceName.value!!, it.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
model.setQueryAndSearch(it.toQuery())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,12 +297,9 @@ class MainActivity :
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val galleryID = editText.text.toString()
|
||||
|
||||
GalleryDialogFragment(source.name, galleryID).apply {
|
||||
GalleryDialogFragment(model.sourceName.value!!, galleryID).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
model.setQueryAndSearch(it.toQuery())
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
||||
@@ -309,16 +311,16 @@ class MainActivity :
|
||||
with (binding.contents.swipePageTurnView) {
|
||||
setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener {
|
||||
override fun onPrev(page: Int) {
|
||||
currentPage--
|
||||
model.prevPage()
|
||||
|
||||
// disable pageturn until the contents are loaded
|
||||
setCurrentPage(1, false)
|
||||
|
||||
query()
|
||||
model.query()
|
||||
}
|
||||
|
||||
override fun onNext(page: Int) {
|
||||
currentPage++
|
||||
model.nextPage()
|
||||
|
||||
// disable pageturn until the contents are loaded
|
||||
setCurrentPage(1, false)
|
||||
@@ -328,30 +330,25 @@ class MainActivity :
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.translationY(0F)
|
||||
|
||||
query()
|
||||
model.query()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setupSearchBar()
|
||||
setupRecyclerView()
|
||||
setSource(sources.values.first())
|
||||
query()
|
||||
// TODO: Save recent source
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupRecyclerView() {
|
||||
with (binding.contents.recyclerview) {
|
||||
adapter = SearchResultsAdapter(searchResults).apply {
|
||||
adapter = SearchResultsAdapter(model.searchResults).apply {
|
||||
onChipClickedHandler = {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
model.setQueryAndSearch(it.toQuery())
|
||||
}
|
||||
onDownloadClickedHandler = { source, itemID ->
|
||||
|
||||
|
||||
closeAllItems()
|
||||
}
|
||||
|
||||
@@ -366,7 +363,7 @@ class MainActivity :
|
||||
return@listener
|
||||
|
||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
|
||||
putExtra("source", source.name)
|
||||
putExtra("source", model.sourceName.value!!)
|
||||
putExtra("id", searchResults[position].id)
|
||||
}
|
||||
|
||||
@@ -380,12 +377,9 @@ class MainActivity :
|
||||
|
||||
val result = searchResults.getOrNull(position) ?: return@listener true
|
||||
|
||||
GalleryDialogFragment(source.name, result.id).apply {
|
||||
GalleryDialogFragment(model.sourceName.value!!, result.id).apply {
|
||||
onChipClickedHandler.add {
|
||||
query = it.toQuery()
|
||||
currentPage = 1
|
||||
|
||||
query()
|
||||
model.setQueryAndSearch(it.toQuery())
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "GalleryDialogFragment")
|
||||
@@ -396,7 +390,6 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private var suggestionJob : Job? = null
|
||||
private fun setupSearchBar() {
|
||||
with (binding.contents.searchview) {
|
||||
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
|
||||
@@ -414,29 +407,15 @@ class MainActivity :
|
||||
}
|
||||
|
||||
onQueryChangeListener = lambda@{ _, query ->
|
||||
this@MainActivity.query = query
|
||||
model.query.value = query
|
||||
|
||||
suggestionJob?.cancel()
|
||||
model.suggestion()
|
||||
|
||||
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 {
|
||||
source.suggestion(currentQuery)
|
||||
}.getOrElse { emptyList() }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
swapSuggestions(if (suggestions.isNotEmpty()) suggestions else listOf(NoResultSuggestion(getText(R.string.main_no_result).toString())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionBinding = { binding, item ->
|
||||
source.onSuggestionBind(binding, item)
|
||||
model.source.value!!.onSuggestionBind(binding, item)
|
||||
}
|
||||
|
||||
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
|
||||
@@ -445,10 +424,8 @@ class MainActivity :
|
||||
}
|
||||
|
||||
override fun onFocusCleared() {
|
||||
suggestionJob?.cancel()
|
||||
|
||||
currentPage = 1
|
||||
query()
|
||||
model.setPage(1)
|
||||
model.query()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,21 +435,19 @@ class MainActivity :
|
||||
|
||||
private fun onActionMenuItemSelected(item: MenuItem?) {
|
||||
if (item?.groupId == R.id.sort_mode_group_id) {
|
||||
currentPage = 1
|
||||
sortMode = source.availableSortMode.let { availableSortMode ->
|
||||
model.setPage(1)
|
||||
model.sortMode.value = model.availableSortMode.value?.let { availableSortMode ->
|
||||
availableSortMode.getOrElse(item.itemId) { availableSortMode.first() }
|
||||
}
|
||||
|
||||
query()
|
||||
model.query()
|
||||
}
|
||||
else
|
||||
when(item?.itemId) {
|
||||
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
|
||||
R.id.source -> SourceSelectDialog().apply {
|
||||
onSourceSelectedListener = {
|
||||
setSource(it)
|
||||
|
||||
query()
|
||||
model.setSourceAndReset(it)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
@@ -485,6 +460,9 @@ class MainActivity :
|
||||
binding.drawer.closeDrawers()
|
||||
|
||||
when(item.itemId) {
|
||||
R.id.main_drawer_history -> {
|
||||
//model.setSourceAndReset(direct.instance<String, History>(arg = source.name))
|
||||
}
|
||||
R.id.main_drawer_help -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
|
||||
}
|
||||
@@ -505,55 +483,4 @@ class MainActivity :
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun cancelFetch() {
|
||||
searchJob?.cancel()
|
||||
}
|
||||
|
||||
private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
|
||||
searchResults.clear()
|
||||
|
||||
(binding.contents.recyclerview.adapter as RecyclerSwipeAdapter).mItemManger.closeAllItems()
|
||||
binding.contents.recyclerview.adapter?.notifyDataSetChanged()
|
||||
|
||||
binding.contents.noresult.visibility = View.INVISIBLE
|
||||
binding.contents.progressbar.show()
|
||||
|
||||
ViewCompat.animate(binding.contents.searchview)
|
||||
.setDuration(100)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.translationY(0F)
|
||||
}
|
||||
private fun query() {
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
|
||||
cancelFetch()
|
||||
clearGalleries()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
searchJob = async(Dispatchers.IO) {
|
||||
source.search(
|
||||
query + Preferences["default_query", ""],
|
||||
(currentPage - 1) * perPage until currentPage * perPage,
|
||||
sortMode
|
||||
)
|
||||
}.also {
|
||||
it.await().let { r ->
|
||||
totalItems = r.second
|
||||
r.first
|
||||
}.let { channel ->
|
||||
binding.contents.progressbar.hide()
|
||||
binding.contents.swipePageTurnView.setCurrentPage(currentPage, totalItems > currentPage*perPage)
|
||||
|
||||
for (result in channel) {
|
||||
searchResults.add(result)
|
||||
binding.contents.recyclerview.adapter?.notifyItemInserted(searchResults.size)
|
||||
}
|
||||
}
|
||||
|
||||
if (searchResults.isEmpty())
|
||||
binding.contents.noresult.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
@@ -38,10 +39,11 @@ import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.databinding.ReaderActivityBinding
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.SavedSourceSet
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
class ReaderActivity : BaseActivity(), DIAware {
|
||||
|
||||
@@ -64,6 +66,9 @@ class ReaderActivity : BaseActivity(), DIAware {
|
||||
private lateinit var binding: ReaderActivityBinding
|
||||
private val model: ReaderViewModel by viewModels()
|
||||
|
||||
private val favorites: SavedSourceSet by instance(tag = "favorites")
|
||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ReaderActivityBinding.inflate(layoutInflater)
|
||||
@@ -78,8 +83,11 @@ class ReaderActivity : BaseActivity(), DIAware {
|
||||
return
|
||||
}
|
||||
|
||||
histories.add(source, itemID)
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
|
||||
|
||||
Logger.d(histories)
|
||||
|
||||
model.readerItems.observe(this) {
|
||||
(binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList())
|
||||
|
||||
@@ -135,11 +143,11 @@ class ReaderActivity : BaseActivity(), DIAware {
|
||||
menu?.forEach {
|
||||
when (it.itemId) {
|
||||
R.id.reader_menu_favorite -> {
|
||||
if (favorites.contains(itemID))
|
||||
if (favorites.map[source]?.contains(itemID) == true)
|
||||
(it.icon as Animatable).start()
|
||||
}
|
||||
R.id.source -> {
|
||||
it.setIcon(direct.instance<AnySource>(tag = source).iconResID)
|
||||
it.setIcon(source(source).value.iconResID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,11 +162,11 @@ class ReaderActivity : BaseActivity(), DIAware {
|
||||
val id = itemID
|
||||
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
||||
|
||||
if (favorites.contains(id)) {
|
||||
favorites.remove(id)
|
||||
if (favorites.map[source]?.contains(id) == true) {
|
||||
favorites.remove(source, id)
|
||||
favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
|
||||
} else {
|
||||
favorites.add(id)
|
||||
favorites.add(source, id)
|
||||
(favorite.icon as Animatable).start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ class DefaultQueryDialogFragment() : DialogFragment() {
|
||||
|
||||
initView()
|
||||
|
||||
return AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.default_query_dialog_title)
|
||||
setView(binding.root)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
return AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.default_query_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newTags = Tags.parse(binding.edittext.text.toString())
|
||||
|
||||
with (binding.languageSelector) {
|
||||
@@ -75,8 +75,7 @@ class DefaultQueryDialogFragment() : DialogFragment() {
|
||||
}
|
||||
|
||||
onPositiveButtonClickListener?.invoke(newTags)
|
||||
}
|
||||
}.create()
|
||||
}.create()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -30,32 +30,39 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.BaseControllerListener
|
||||
import com.facebook.imagepipeline.image.ImageInfo
|
||||
import com.orhanobut.logger.Logger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.di
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.SearchResultsAdapter
|
||||
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
|
||||
import xyz.quaver.pupil.databinding.*
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import xyz.quaver.pupil.ui.view.TagChip
|
||||
import xyz.quaver.pupil.ui.viewmodel.GalleryDialogViewModel
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
import xyz.quaver.pupil.util.SavedSourceSet
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class GalleryDialogFragment(private val source: String, private val itemID: String) : DialogFragment() {
|
||||
class GalleryDialogFragment(private val source: String, private val itemID: String) : DialogFragment(), DIAware {
|
||||
|
||||
override val di by di()
|
||||
|
||||
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
|
||||
|
||||
@@ -150,7 +157,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
||||
info.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.sortedBy {
|
||||
val tag = Tag.parse(it)
|
||||
|
||||
if (favoriteTags.contains(tag))
|
||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
||||
-1
|
||||
else
|
||||
when(Tag.parse(it).area) {
|
||||
@@ -175,7 +182,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
||||
|
||||
content!!.forEach { tag ->
|
||||
tags.addView(
|
||||
TagChip(requireContext(), tag).apply {
|
||||
TagChip(requireContext(), source, tag).apply {
|
||||
setOnClickListener {
|
||||
onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
@@ -210,7 +217,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
|
||||
}
|
||||
|
||||
private fun addRelated(relatedItems: List<ItemInfo>) {
|
||||
val adapter = SearchResultsAdapter(relatedItems).apply {
|
||||
val adapter = SearchResultsAdapter(MutableLiveData(relatedItems)).apply {
|
||||
onChipClickedHandler = { tag ->
|
||||
this@GalleryDialogFragment.onChipClickedHandler.forEach { handler ->
|
||||
handler.invoke(tag)
|
||||
|
||||
@@ -25,14 +25,20 @@ import android.view.Window
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.android.x.di
|
||||
import org.kodein.type.jvmType
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.adapters.SourceAdapter
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.sources
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
|
||||
class SourceSelectDialog : DialogFragment() {
|
||||
class SourceSelectDialog : DialogFragment(), DIAware {
|
||||
|
||||
var onSourceSelectedListener: ((Source<*, SearchSuggestion>) -> Unit)? = null
|
||||
override val di by di()
|
||||
|
||||
var onSourceSelectedListener: ((String) -> Unit)? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return Dialog(requireContext()).apply {
|
||||
@@ -41,7 +47,7 @@ class SourceSelectDialog : DialogFragment() {
|
||||
|
||||
setContentView(RecyclerView(context).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = SourceAdapter(sources.values.toList()).apply {
|
||||
adapter = SourceAdapter(direct.instance()).apply {
|
||||
onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,14 +28,17 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import okhttp3.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.di
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import xyz.quaver.pupil.util.restore
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
|
||||
|
||||
override val di by di()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
|
||||
@@ -87,7 +90,7 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
|
||||
.setTitle(R.string.settings_restore_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
restore(editText.text.toString(),
|
||||
restore(context, editText.text.toString(),
|
||||
onFailure = onFailure@{
|
||||
val view = view ?: return@onFailure
|
||||
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
|
||||
|
||||
@@ -28,11 +28,7 @@ import org.kodein.di.android.x.di
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.histories
|
||||
import xyz.quaver.pupil.util.DownloadManager
|
||||
import xyz.quaver.pupil.util.ImageCache
|
||||
import xyz.quaver.pupil.util.byteToString
|
||||
import xyz.quaver.pupil.util.size
|
||||
import xyz.quaver.pupil.util.*
|
||||
|
||||
class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.OnPreferenceClickListener {
|
||||
|
||||
@@ -43,6 +39,8 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
||||
private val downloadManager: DownloadManager by instance()
|
||||
private val cache: ImageCache by instance()
|
||||
|
||||
private val histories: SavedSourceSet by instance(tag = "histories")
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey)
|
||||
|
||||
@@ -122,7 +120,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
||||
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)
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.map.values.sumOf { it.size })
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
@@ -169,7 +167,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
|
||||
with (findPreference<Preference>("clear_history")) {
|
||||
this ?: return@with
|
||||
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.size)
|
||||
summary = context.getString(R.string.settings_clear_history_summary, histories.map.values.sumOf { it.size })
|
||||
|
||||
onPreferenceClickListener = this@ManageStorageFragment
|
||||
}
|
||||
|
||||
@@ -21,28 +21,23 @@ 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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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.databinding.SearchSuggestionItemBinding
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.SuggestionCountBinding
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.sources.DefaultSearchSuggestion
|
||||
import xyz.quaver.pupil.sources.Hitomi
|
||||
import xyz.quaver.pupil.types.*
|
||||
import xyz.quaver.pupil.types.FavoriteHistorySwitch
|
||||
import xyz.quaver.pupil.types.HistorySuggestion
|
||||
import xyz.quaver.pupil.types.LoadingSuggestion
|
||||
import xyz.quaver.pupil.types.NoResultSuggestion
|
||||
import java.util.*
|
||||
|
||||
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
|
||||
@@ -22,15 +22,22 @@ import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.favoriteTags
|
||||
import xyz.quaver.pupil.sources.Hitomi
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.SavedSourceSet
|
||||
import xyz.quaver.pupil.util.translations
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(context), DIAware {
|
||||
|
||||
override val di by di(context)
|
||||
|
||||
private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags")
|
||||
|
||||
val tag: Tag =
|
||||
_tag.let {
|
||||
@@ -56,20 +63,20 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
}
|
||||
}
|
||||
|
||||
if (favoriteTags.contains(tag))
|
||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
||||
|
||||
isCloseIconVisible = true
|
||||
closeIcon = ContextCompat.getDrawable(context,
|
||||
if (favoriteTags.contains(tag))
|
||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true)
|
||||
R.drawable.ic_star_filled
|
||||
else
|
||||
R.drawable.ic_star_empty
|
||||
)
|
||||
|
||||
setOnCloseIconClickListener {
|
||||
if (favoriteTags.contains(tag)) {
|
||||
favoriteTags.remove(tag)
|
||||
if (favoriteTags.map[source]?.contains(tag.toString()) == true) {
|
||||
favoriteTags.remove(source, tag.toString())
|
||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
|
||||
|
||||
when(tag.area) {
|
||||
@@ -78,7 +85,7 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
|
||||
else -> chipBackgroundColor = null
|
||||
}
|
||||
} else {
|
||||
favoriteTags.add(tag)
|
||||
favoriteTags.add(source, tag.toString())
|
||||
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
|
||||
setChipBackgroundColorResource(R.color.material_orange_500)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 {
|
||||
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, var source: String = "hitomi.la", val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
|
||||
|
||||
object Defaults {
|
||||
const val maxChipSize = 10
|
||||
@@ -53,7 +53,7 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
|
||||
for (i in maxChipSize until tags.size) {
|
||||
val tag = tags.elementAt(i)
|
||||
|
||||
addView(TagChip(context, tag).apply {
|
||||
addView(TagChip(context, source, tag).apply {
|
||||
setOnClickListener {
|
||||
onClickListener?.invoke(tag)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
|
||||
refreshJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
tags.take(maxChipSize).map {
|
||||
CoroutineScope(Dispatchers.Default).async {
|
||||
TagChip(context, it).apply {
|
||||
TagChip(context, source, it).apply {
|
||||
setOnClickListener {
|
||||
onClickListener?.invoke(this.tag)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.kodein.di.android.x.di
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
|
||||
@@ -41,7 +42,7 @@ class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware
|
||||
val related: LiveData<List<ItemInfo>> = _related
|
||||
|
||||
fun load(source: String, itemID: String) {
|
||||
val source: AnySource by instance(tag = source)
|
||||
val source: AnySource by source(source)
|
||||
|
||||
viewModelScope.launch {
|
||||
_info.value = withContext(Dispatchers.IO) {
|
||||
|
||||
174
app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
Normal file
174
app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 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.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.di
|
||||
import org.kodein.di.direct
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.notify
|
||||
import xyz.quaver.pupil.util.source
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
|
||||
override val di by di()
|
||||
|
||||
private val _searchResults = MutableLiveData<MutableList<ItemInfo>>()
|
||||
val searchResults = _searchResults as LiveData<List<ItemInfo>>
|
||||
var loading = false
|
||||
private set
|
||||
|
||||
private var queryJob: Job? = null
|
||||
private var suggestionJob: Job? = null
|
||||
|
||||
val query = MutableLiveData<String>()
|
||||
private val queryStack = mutableListOf<String>()
|
||||
|
||||
private val _source = MutableLiveData<AnySource>()
|
||||
val source: LiveData<AnySource> = _source
|
||||
|
||||
val availableSortMode = Transformations.map(_source) {
|
||||
it.availableSortMode
|
||||
}
|
||||
val sortMode = MutableLiveData<Enum<*>>()
|
||||
|
||||
val sourceIcon = Transformations.map(_source) {
|
||||
it.iconResID
|
||||
}
|
||||
|
||||
val sourceName = Transformations.map(_source) {
|
||||
it.name
|
||||
}
|
||||
|
||||
private val _currentPage = MutableLiveData<Int>()
|
||||
val currentPage: LiveData<Int> = _currentPage
|
||||
|
||||
private val totalItems = MutableLiveData<Int>()
|
||||
|
||||
val totalPages = Transformations.map(totalItems) {
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
|
||||
ceil(it / perPage.toDouble()).roundToInt()
|
||||
}
|
||||
|
||||
private val _suggestions = MutableLiveData<List<SearchSuggestion>>()
|
||||
val suggestions: LiveData<List<SearchSuggestion>> = _suggestions
|
||||
|
||||
init {
|
||||
setSourceAndReset("hitomi.la")
|
||||
}
|
||||
|
||||
fun setSourceAndReset(sourceName: String) {
|
||||
_source.value = direct.source(sourceName).also {
|
||||
sortMode.value = it.availableSortMode.first()
|
||||
}
|
||||
|
||||
setQueryAndSearch()
|
||||
}
|
||||
|
||||
fun setQueryAndSearch(query: String = "") {
|
||||
this.query.value = query
|
||||
setPage(1)
|
||||
|
||||
query()
|
||||
}
|
||||
|
||||
fun query() {
|
||||
val perPage = Preferences["per_page", "25"].toInt()
|
||||
val source = _source.value ?: error("Source is null")
|
||||
val sortMode = sortMode.value ?: source.availableSortMode.first()
|
||||
val currentPage = currentPage.value ?: 1
|
||||
|
||||
suggestionJob?.cancel()
|
||||
queryJob?.cancel()
|
||||
|
||||
loading = true
|
||||
val results = mutableListOf<ItemInfo>()
|
||||
_searchResults.value = results
|
||||
|
||||
queryJob = viewModelScope.launch {
|
||||
val channel = withContext(Dispatchers.IO) {
|
||||
val (channel, count) = source.search(
|
||||
(query.value ?: "") + Preferences["default_query", ""],
|
||||
(currentPage - 1) * perPage until currentPage * perPage,
|
||||
sortMode
|
||||
)
|
||||
|
||||
totalItems.postValue(count)
|
||||
|
||||
channel
|
||||
}
|
||||
|
||||
for (result in channel) {
|
||||
yield()
|
||||
results.add(result)
|
||||
_searchResults.notify()
|
||||
}
|
||||
_searchResults.notify()
|
||||
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun prevPage() { _currentPage.value = _currentPage.value!! - 1 }
|
||||
fun nextPage() { _currentPage.value = _currentPage.value!! + 1 }
|
||||
fun setPage(page: Int) { _currentPage.value = page }
|
||||
|
||||
fun random(callback: (ItemInfo) -> Unit) {
|
||||
if (totalItems.value!! == 0)
|
||||
return
|
||||
|
||||
val random = Random.Default.nextInt(totalItems.value!!)
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
_source.value?.search(
|
||||
query.value + Preferences["default_query", ""],
|
||||
random .. random,
|
||||
sortMode.value!!
|
||||
)?.first?.receive()
|
||||
}?.let(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun suggestion() {
|
||||
suggestionJob?.cancel()
|
||||
|
||||
_suggestions.value = mutableListOf()
|
||||
|
||||
suggestionJob = viewModelScope.launch {
|
||||
_suggestions.value = withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
_source.value!!.suggestion(query.value!!)
|
||||
}.getOrElse { emptyList() }
|
||||
}!!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import xyz.quaver.pupil.adapters.ReaderItem
|
||||
import xyz.quaver.pupil.sources.AnySource
|
||||
import xyz.quaver.pupil.util.ImageCache
|
||||
import xyz.quaver.pupil.util.notify
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
@@ -53,7 +54,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun load(sourceName: String, itemID: String) {
|
||||
val source: AnySource by instance(tag = sourceName)
|
||||
val source: AnySource by source(sourceName)
|
||||
|
||||
viewModelScope.launch {
|
||||
_title.value = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -83,7 +83,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D
|
||||
|
||||
@Synchronized
|
||||
fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
|
||||
val source: AnySource by instance(tag = source)
|
||||
val source: AnySource by source(source)
|
||||
val info = async { source.info(itemID) }
|
||||
val images = async { source.images(itemID) }
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.Json.Default.decodeFromString
|
||||
import kotlinx.serialization.serializer
|
||||
import java.io.File
|
||||
|
||||
@@ -44,7 +46,7 @@ class SavedSet <T: Any> (private val file: File, any: T, private val set: Mutabl
|
||||
fun load() {
|
||||
set.clear()
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
set.addAll(it)
|
||||
}
|
||||
@@ -111,7 +113,7 @@ class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V,
|
||||
fun load() {
|
||||
map.clear()
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString(serializer, file.readText())
|
||||
decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
map.putAll(it)
|
||||
}
|
||||
@@ -167,4 +169,79 @@ class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V,
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SavedSourceSet(private val file: File) {
|
||||
|
||||
private val _map = mutableMapOf<String, MutableSet<String>>()
|
||||
val map: Map<String, Set<String>> = _map
|
||||
|
||||
private val serializer = MapSerializer(String.serializer(), SetSerializer(String.serializer()))
|
||||
|
||||
@Synchronized
|
||||
fun load() {
|
||||
_map.clear()
|
||||
kotlin.runCatching {
|
||||
decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
it.forEach { (k, v) ->
|
||||
_map[k] = v.toMutableSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save() {
|
||||
file.parentFile?.mkdirs()
|
||||
if (!file.exists())
|
||||
file.createNewFile()
|
||||
|
||||
file.writeText(Json.encodeToString(serializer, _map))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun add(source: String, value: String) {
|
||||
load()
|
||||
|
||||
_map[source]?.remove(value)
|
||||
|
||||
if (!_map.containsKey(source))
|
||||
_map[source] = mutableSetOf()
|
||||
else
|
||||
_map[source]!!.add(value)
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addAll(from: Map<String, Set<String>>) {
|
||||
load()
|
||||
|
||||
for (source in from.keys) {
|
||||
if (_map.containsKey(source)) {
|
||||
_map[source]!!.removeAll(from[source]!!)
|
||||
_map[source]!!.addAll(from[source]!!)
|
||||
} else {
|
||||
_map[source] = from[source]!!.toMutableSet()
|
||||
}
|
||||
}
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun remove(source: String, value: String): Boolean {
|
||||
load()
|
||||
|
||||
return (_map[source]?.remove(value) ?: false).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
_map.clear()
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,14 +20,17 @@ package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.serialization.json.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.kodein.di.*
|
||||
import xyz.quaver.hitomi.GalleryInfo
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
@@ -128,4 +131,15 @@ fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytes
|
||||
bytes = read(buffer)
|
||||
}
|
||||
return bytesCopied
|
||||
}
|
||||
|
||||
fun DIAware.source(source: String) = lazy { direct.source(source) }
|
||||
fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!!
|
||||
|
||||
fun View.hide() {
|
||||
visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
fun View.show() {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@@ -18,37 +18,33 @@
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
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.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.di
|
||||
import org.kodein.di.instance
|
||||
import ru.noties.markwon.Markwon
|
||||
import xyz.quaver.pupil.BuildConfig
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.client
|
||||
import xyz.quaver.pupil.favorites
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
@@ -184,7 +180,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<String>) -> Unit)? = null) {
|
||||
fun restore(context: Context, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Set<String>) -> Unit)? = null) {
|
||||
if (!URLUtil.isValidUrl(url)) {
|
||||
onFailure?.invoke(IllegalArgumentException())
|
||||
return
|
||||
@@ -201,9 +197,10 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val favorites = object: DIAware { override val di by di(context); val favorites: SavedSourceSet by instance(tag = "favorites") }
|
||||
kotlin.runCatching {
|
||||
Json.decodeFromString<List<String>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
|
||||
favorites.addAll(it)
|
||||
Json.decodeFromString<Set<String>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
|
||||
favorites.favorites.addAll(mapOf("hitomi.la" to it))
|
||||
onSuccess?.invoke(it)
|
||||
}
|
||||
}.onFailure { onFailure?.invoke(it) }
|
||||
|
||||
Reference in New Issue
Block a user