This commit is contained in:
tom5079
2021-02-11 19:24:40 +09:00
parent c7b3ae7ed1
commit 5a61fcf6ee
34 changed files with 656 additions and 329 deletions

View File

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

View File

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

View File

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

View File

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

View 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() {
}
}

View 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 {
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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() }
}!!
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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