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

@@ -76,5 +76,10 @@
<option name="name" value="maven" /> <option name="name" value="maven" />
<option name="url" value="https://dl.bintray.com/piasy/maven" /> <option name="url" value="https://dl.bintray.com/piasy/maven" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
</remote-repository>
</component> </component>
</project> </project>

View File

@@ -104,7 +104,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0-rc01" implementation "com.google.android.material:material:1.3.0"
implementation platform("com.google.firebase:firebase-bom:26.1.0") implementation platform("com.google.firebase:firebase-bom:26.1.0")
implementation "com.google.firebase:firebase-analytics-ktx" implementation "com.google.firebase:firebase-analytics-ktx"
@@ -134,7 +134,7 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0" implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.9.7" implementation "xyz.quaver:libpupil:1.9.7-SNAPSHOT"
implementation "xyz.quaver:documentfilex:0.4-alpha02" implementation "xyz.quaver:documentfilex:0.4-alpha02"
implementation "xyz.quaver:floatingsearchview:1.1.1" implementation "xyz.quaver:floatingsearchview:1.1.1"

View File

@@ -42,9 +42,6 @@
</provider> </provider>
<service android:name=".services.DownloadService"
android:exported="false"/>
<receiver <receiver
android:name=".receiver.UpdateBroadcastReceiver" android:name=".receiver.UpdateBroadcastReceiver"
android:exported="true"> android:exported="true">

View File

@@ -44,23 +44,12 @@ import okhttp3.*
import org.kodein.di.* import org.kodein.di.*
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.sources.initSources
import xyz.quaver.pupil.sources.sourceModule import xyz.quaver.pupil.sources.sourceModule
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.setClient import xyz.quaver.setClient
import java.io.File import java.io.File
import java.util.* 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 lateinit var clientBuilder: OkHttpClient.Builder
var clientHolder: OkHttpClient? = null var clientHolder: OkHttpClient? = null
@@ -79,6 +68,11 @@ class Pupil : Application(), DIAware {
bind<OkHttpClient>() with provider { client } bind<OkHttpClient>() with provider { client }
bind<ImageCache>() with singleton { ImageCache(this@Pupil) } bind<ImageCache>() with singleton { ImageCache(this@Pupil) }
bind<DownloadManager>() with singleton { DownloadManager(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 private lateinit var firebaseAnalytics: FirebaseAnalytics
@@ -93,8 +87,6 @@ class Pupil : Application(), DIAware {
else userID else userID
} }
initSources(this)
firebaseAnalytics = Firebase.analytics firebaseAnalytics = Firebase.analytics
FirebaseCrashlytics.getInstance().setUserId(userID) FirebaseCrashlytics.getInstance().setUserId(userID)
@@ -125,11 +117,6 @@ class Pupil : Application(), DIAware {
Preferences["reset_secure"] = true 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) if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false) FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)

View File

@@ -60,8 +60,12 @@ class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderIt
with (binding.image) { with (binding.image) {
setImageViewFactory(FrescoImageViewFactory().apply { setImageViewFactory(FrescoImageViewFactory().apply {
updateView = { imageInfo -> updateView = { imageInfo ->
layoutParams.height = imageInfo.height if (!fullscreen) {
(mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat() binding.root.layoutParams.height = imageInfo.height
layoutParams.height = imageInfo.height
(mainView as? SimpleDraweeView)?.aspectRatio = imageInfo.width / imageInfo.height.toFloat()
}
} }
}) })
setImageShownCallback(object: ImageShownCallback { setImageShownCallback(object: ImageShownCallback {

View File

@@ -27,6 +27,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.daimajia.swipe.SwipeLayout import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter 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.drawee.controller.BaseControllerListener
import com.facebook.imagepipeline.image.ImageInfo import com.facebook.imagepipeline.image.ImageInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.kodein.di.DIAware
import org.kodein.di.android.di
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.SearchResultItemBinding import xyz.quaver.pupil.databinding.SearchResultItemBinding
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import kotlin.time.ExperimentalTime 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 onChipClickedHandler: ((Tag) -> Unit)? = null
var onDownloadClickedHandler: ((source: String, itemI: String) -> 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 { binding.idView.setOnClickListener {
// TODO: MEMLEAK
(itemView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( (itemView.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("item_id", itemID) ClipData.newPlainText("item_id", itemID)
) )
@@ -146,6 +150,7 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
with (binding.tagGroup) { with (binding.tagGroup) {
tags.clear() tags.clear()
source = result.source
result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.let { if (it.size == 1 && it.first().isEmpty()) emptyList() else it }?.map { result.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.let { if (it.size == 1 && it.first().isEmpty()) emptyList() else it }?.map {
Tag.parse(it) Tag.parse(it)
}?.let { tags.addAll(it) } }?.let { tags.addAll(it) }
@@ -201,10 +206,10 @@ class SearchResultsAdapter(private val results: List<ItemInfo>) : RecyclerSwipeA
@ExperimentalTime @ExperimentalTime
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
mItemManger.bindView(holder.itemView, position) 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 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 androidx.recyclerview.widget.RecyclerView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
import xyz.quaver.pupil.sources.AnySource
import xyz.quaver.pupil.sources.Source 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) { inner class ViewHolder(private val binding: SourceSelectDialogItemBinding) : RecyclerView.ViewHolder(binding.root) {
lateinit var source: Source<*, SearchSuggestion> lateinit var source: AnySource
init { init {
binding.go.setOnClickListener { binding.go.setOnClickListener {
onSourceSelectedListener?.invoke(source) onSourceSelectedListener?.invoke(source.name)
} }
} }
fun bind(source: Source<*, SearchSuggestion>) { fun bind(source: AnySource) {
this.source = source this.source = source
binding.icon.setImageDrawable(sourceIcons[source.name]) // TODO: save image somewhere else
binding.icon.setImageResource(source.iconResID)
binding.name.text = source.name 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) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(sources[position]) holder.bind(sources[position].second)
} }
override fun getItemCount(): Int = sources.size 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.Serializable
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import org.kodein.di.DI import org.kodein.di.*
import org.kodein.di.bind
import org.kodein.di.contexted
import org.kodein.di.instance
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
@@ -139,29 +136,18 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
} }
} }
@Deprecated("") typealias SourceEntry = Pair<String, AnySource>
val sources = mutableMapOf<String, AnySource>() typealias SourceEntries = Set<SourceEntry>
val sourceIcons = mutableMapOf<String, Drawable?>()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val sourceModule = DI.Module(name = "source") { val sourceModule = DI.Module(name = "source") {
listOf( bind() from setBinding<SourceEntry>()
Hitomi(),
Hiyobi()
).forEach {
bind<AnySource>(tag = it.name) with instance (it as AnySource)
}
}
@Deprecated("")
@Suppress("UNCHECKED_CAST")
fun initSources(context: Context) {
// Add Default Sources
listOf( listOf(
Hitomi(), Hitomi(),
Hiyobi() Hiyobi()
).forEach { ).forEach { source ->
sources[it.name] = it as AnySource bind<SourceEntry>().inSet() with multiton { _: Unit -> source.name to (source as AnySource) }
sourceIcons[it.name] = ContextCompat.getDrawable(context, it.iconResID)
} }
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> { override suspend fun images(itemID: String): List<String> {
return createImgList(itemID, getGalleryInfo(itemID), false).map { return createImgList(itemID, getGalleryInfo(itemID), true).map {
it.path it.path
} }
} }

View File

@@ -70,12 +70,12 @@ class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags
companion object { companion object {
fun parse(tags: String) : Tags { fun parse(tags: String) : Tags {
return Tags( return Tags(
tags.split(' ').map { tags.split(' ').mapNotNull {
if (it.isNotEmpty()) if (it.isNotEmpty())
Tag.parse(it) Tag.parse(it)
else else
null null
}.filterNotNull().toMutableSet() }.toMutableSet()
) )
} }
} }

View File

@@ -27,11 +27,11 @@ import android.text.util.Linkify
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -42,53 +42,38 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.orhanobut.logger.Logger
import kotlinx.coroutines.* 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.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.* import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.SearchResultsAdapter import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.sources.ItemInfo 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.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialogFragment import xyz.quaver.pupil.ui.dialog.GalleryDialogFragment
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.view.ProgressCardView import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.view.SwipePageTurnView import xyz.quaver.pupil.ui.view.SwipePageTurnView
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.* import kotlin.math.*
import kotlin.random.Random
class MainActivity : class MainActivity :
BaseActivity(), BaseActivity(),
NavigationView.OnNavigationItemSelectedListener NavigationView.OnNavigationItemSelectedListener,
DIAware
{ {
private val searchResults = mutableListOf<ItemInfo>() override val di by di()
private var query = "" private val searchResults = mutableListOf<ItemInfo>()
set(value) {
field = value
with (findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
}
private var queryStack = mutableListOf<String>() private var 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 lateinit var binding: MainActivityBinding
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -97,7 +82,7 @@ class MainActivity :
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url -> intent.dataString?.let { url ->
restore(url, restore(this, url,
onFailure = { onFailure = {
Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = { }, onSuccess = {
@@ -113,6 +98,74 @@ class MainActivity :
checkUpdate(this) checkUpdate(this)
initView() 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() { override fun onDestroy() {
@@ -126,36 +179,31 @@ class MainActivity :
when { when {
binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START) binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last() model.query.value = queryStack.last()
query() model.query()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 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) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 1) { if (model.currentPage.value!! > 1) {
runOnUiThread { runOnUiThread {
currentPage-- model.prevPage()
model.query()
query()
} }
} }
true true
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) { if (model.currentPage.value!! < model.totalPages.value!!) {
runOnUiThread { runOnUiThread {
currentPage++ model.nextPage()
model.query()
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() { private fun initView() {
binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() { binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@@ -234,14 +254,13 @@ class MainActivity :
setTitle(R.string.main_jump_title) setTitle(R.string.main_jump_title)
setMessage(getString( setMessage(getString(
R.string.main_jump_message, R.string.main_jump_message,
currentPage, model.currentPage.value!!,
ceil(totalItems / perPage.toDouble()).roundToInt() ceil(model.totalPages.value!! / perPage.toDouble()).roundToInt()
)) ))
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton) model.setPage(editText.text.toString().toIntOrNull() ?: return@setPositiveButton)
model.query()
query()
} }
}.show() }.show()
} }
@@ -251,30 +270,16 @@ class MainActivity :
setImageResource(R.drawable.shuffle_variant) setImageResource(R.drawable.shuffle_variant)
setOnClickListener { setOnClickListener {
setImageDrawable(CircularProgressDrawable(context)) setImageDrawable(CircularProgressDrawable(context))
if (totalItems > 0)
CoroutineScope(Dispatchers.IO).launch {
val random = Random.Default.nextInt(totalItems)
val randomResult = model.random { runOnUiThread {
source.search( setImageResource(R.drawable.shuffle_variant)
query + Preferences["default_query", ""], GalleryDialogFragment(model.sourceName.value!!, it.id).apply {
random .. random, onChipClickedHandler.add {
sortMode model.setQueryAndSearch(it.toQuery())
).first.receive() dismiss()
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")
} }
} }.show(supportFragmentManager, "GalleryDialogFragment")
} }
} }
} }
@@ -292,12 +297,9 @@ class MainActivity :
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString() val galleryID = editText.text.toString()
GalleryDialogFragment(source.name, galleryID).apply { GalleryDialogFragment(model.sourceName.value!!, galleryID).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
query = it.toQuery() model.setQueryAndSearch(it.toQuery())
currentPage = 1
query()
dismiss() dismiss()
} }
}.show(supportFragmentManager, "GalleryDialogFragment") }.show(supportFragmentManager, "GalleryDialogFragment")
@@ -309,16 +311,16 @@ class MainActivity :
with (binding.contents.swipePageTurnView) { with (binding.contents.swipePageTurnView) {
setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener { setOnPageTurnListener(object: SwipePageTurnView.OnPageTurnListener {
override fun onPrev(page: Int) { override fun onPrev(page: Int) {
currentPage-- model.prevPage()
// disable pageturn until the contents are loaded // disable pageturn until the contents are loaded
setCurrentPage(1, false) setCurrentPage(1, false)
query() model.query()
} }
override fun onNext(page: Int) { override fun onNext(page: Int) {
currentPage++ model.nextPage()
// disable pageturn until the contents are loaded // disable pageturn until the contents are loaded
setCurrentPage(1, false) setCurrentPage(1, false)
@@ -328,30 +330,25 @@ class MainActivity :
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.translationY(0F) .translationY(0F)
query() model.query()
} }
}) })
} }
setupSearchBar() setupSearchBar()
setupRecyclerView() setupRecyclerView()
setSource(sources.values.first()) // TODO: Save recent source
query()
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() { private fun setupRecyclerView() {
with (binding.contents.recyclerview) { with (binding.contents.recyclerview) {
adapter = SearchResultsAdapter(searchResults).apply { adapter = SearchResultsAdapter(model.searchResults).apply {
onChipClickedHandler = { onChipClickedHandler = {
query = it.toQuery() model.setQueryAndSearch(it.toQuery())
currentPage = 1
query()
} }
onDownloadClickedHandler = { source, itemID -> onDownloadClickedHandler = { source, itemID ->
closeAllItems() closeAllItems()
} }
@@ -366,7 +363,7 @@ class MainActivity :
return@listener return@listener
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply { val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
putExtra("source", source.name) putExtra("source", model.sourceName.value!!)
putExtra("id", searchResults[position].id) putExtra("id", searchResults[position].id)
} }
@@ -380,12 +377,9 @@ class MainActivity :
val result = searchResults.getOrNull(position) ?: return@listener true val result = searchResults.getOrNull(position) ?: return@listener true
GalleryDialogFragment(source.name, result.id).apply { GalleryDialogFragment(model.sourceName.value!!, result.id).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
query = it.toQuery() model.setQueryAndSearch(it.toQuery())
currentPage = 1
query()
dismiss() dismiss()
} }
}.show(supportFragmentManager, "GalleryDialogFragment") }.show(supportFragmentManager, "GalleryDialogFragment")
@@ -396,7 +390,6 @@ class MainActivity :
} }
} }
private var suggestionJob : Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
with (binding.contents.searchview) { with (binding.contents.searchview) {
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
@@ -414,29 +407,15 @@ class MainActivity :
} }
onQueryChangeListener = lambda@{ _, query -> onQueryChangeListener = lambda@{ _, query ->
this@MainActivity.query = query model.query.value = query
suggestionJob?.cancel() model.suggestion()
swapSuggestions(listOf(LoadingSuggestion(getText(R.string.reader_loading).toString()))) 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 -> onSuggestionBinding = { binding, item ->
source.onSuggestionBind(binding, item) model.source.value!!.onSuggestionBind(binding, item)
} }
onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener { onFocusChangeListener = object: FloatingSearchView.OnFocusChangeListener {
@@ -445,10 +424,8 @@ class MainActivity :
} }
override fun onFocusCleared() { override fun onFocusCleared() {
suggestionJob?.cancel() model.setPage(1)
model.query()
currentPage = 1
query()
} }
} }
@@ -458,21 +435,19 @@ class MainActivity :
private fun onActionMenuItemSelected(item: MenuItem?) { private fun onActionMenuItemSelected(item: MenuItem?) {
if (item?.groupId == R.id.sort_mode_group_id) { if (item?.groupId == R.id.sort_mode_group_id) {
currentPage = 1 model.setPage(1)
sortMode = source.availableSortMode.let { availableSortMode -> model.sortMode.value = model.availableSortMode.value?.let { availableSortMode ->
availableSortMode.getOrElse(item.itemId) { availableSortMode.first() } availableSortMode.getOrElse(item.itemId) { availableSortMode.first() }
} }
query() model.query()
} }
else else
when(item?.itemId) { when(item?.itemId) {
R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java)) R.id.main_menu_settings -> startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
R.id.source -> SourceSelectDialog().apply { R.id.source -> SourceSelectDialog().apply {
onSourceSelectedListener = { onSourceSelectedListener = {
setSource(it) model.setSourceAndReset(it)
query()
dismiss() dismiss()
} }
@@ -485,6 +460,9 @@ class MainActivity :
binding.drawer.closeDrawers() binding.drawer.closeDrawers()
when(item.itemId) { when(item.itemId) {
R.id.main_drawer_history -> {
//model.setSourceAndReset(direct.instance<String, History>(arg = source.name))
}
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.help))))
} }
@@ -505,55 +483,4 @@ class MainActivity :
return true 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.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.orhanobut.logger.Logger
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.di import org.kodein.di.android.di
@@ -38,10 +39,11 @@ import org.kodein.di.instance
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.ReaderActivityBinding import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.sources.AnySource import xyz.quaver.pupil.sources.AnySource
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.SavedSourceSet
import xyz.quaver.pupil.util.source
class ReaderActivity : BaseActivity(), DIAware { class ReaderActivity : BaseActivity(), DIAware {
@@ -64,6 +66,9 @@ class ReaderActivity : BaseActivity(), DIAware {
private lateinit var binding: ReaderActivityBinding private lateinit var binding: ReaderActivityBinding
private val model: ReaderViewModel by viewModels() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ReaderActivityBinding.inflate(layoutInflater) binding = ReaderActivityBinding.inflate(layoutInflater)
@@ -78,8 +83,11 @@ class ReaderActivity : BaseActivity(), DIAware {
return return
} }
histories.add(source, itemID)
FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID) FirebaseCrashlytics.getInstance().setCustomKey("GalleryID", itemID)
Logger.d(histories)
model.readerItems.observe(this) { model.readerItems.observe(this) {
(binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList()) (binding.recyclerview.adapter as ReaderAdapter).submitList(it.toMutableList())
@@ -135,11 +143,11 @@ class ReaderActivity : BaseActivity(), DIAware {
menu?.forEach { menu?.forEach {
when (it.itemId) { when (it.itemId) {
R.id.reader_menu_favorite -> { R.id.reader_menu_favorite -> {
if (favorites.contains(itemID)) if (favorites.map[source]?.contains(itemID) == true)
(it.icon as Animatable).start() (it.icon as Animatable).start()
} }
R.id.source -> { 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 id = itemID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
if (favorites.contains(id)) { if (favorites.map[source]?.contains(id) == true) {
favorites.remove(id) favorites.remove(source, id)
favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star) favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
} else { } else {
favorites.add(id) favorites.add(source, id)
(favorite.icon as Animatable).start() (favorite.icon as Animatable).start()
} }
} }

View File

@@ -50,10 +50,10 @@ class DefaultQueryDialogFragment() : DialogFragment() {
initView() initView()
return AlertDialog.Builder(requireContext()).apply { return AlertDialog.Builder(requireContext())
setTitle(R.string.default_query_dialog_title) .setTitle(R.string.default_query_dialog_title)
setView(binding.root) .setView(binding.root)
setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
val newTags = Tags.parse(binding.edittext.text.toString()) val newTags = Tags.parse(binding.edittext.text.toString())
with (binding.languageSelector) { with (binding.languageSelector) {
@@ -75,8 +75,7 @@ class DefaultQueryDialogFragment() : DialogFragment() {
} }
onPositiveButtonClickListener?.invoke(newTags) onPositiveButtonClickListener?.invoke(newTags)
} }.create()
}.create()
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -30,32 +30,39 @@ import androidx.core.content.ContextCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.imagepipeline.image.ImageInfo import com.facebook.imagepipeline.image.ImageInfo
import com.orhanobut.logger.Logger
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock 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.R
import xyz.quaver.pupil.adapters.SearchResultsAdapter import xyz.quaver.pupil.adapters.SearchResultsAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.databinding.* import xyz.quaver.pupil.databinding.*
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.ui.view.TagChip import xyz.quaver.pupil.ui.view.TagChip
import xyz.quaver.pupil.ui.viewmodel.GalleryDialogViewModel import xyz.quaver.pupil.ui.viewmodel.GalleryDialogViewModel
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.SavedSourceSet
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
import java.util.* import java.util.*
import kotlin.collections.ArrayList 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))>() 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 { info.extra[ItemInfo.ExtraType.TAGS]?.await()?.split(", ")?.filterNot { it.isEmpty() }?.sortedBy {
val tag = Tag.parse(it) val tag = Tag.parse(it)
if (favoriteTags.contains(tag)) if (favoriteTags.map[source]?.contains(tag.toString()) == true)
-1 -1
else else
when(Tag.parse(it).area) { when(Tag.parse(it).area) {
@@ -175,7 +182,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
content!!.forEach { tag -> content!!.forEach { tag ->
tags.addView( tags.addView(
TagChip(requireContext(), tag).apply { TagChip(requireContext(), source, tag).apply {
setOnClickListener { setOnClickListener {
onChipClickedHandler.forEach { handler -> onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)
@@ -210,7 +217,7 @@ class GalleryDialogFragment(private val source: String, private val itemID: Stri
} }
private fun addRelated(relatedItems: List<ItemInfo>) { private fun addRelated(relatedItems: List<ItemInfo>) {
val adapter = SearchResultsAdapter(relatedItems).apply { val adapter = SearchResultsAdapter(MutableLiveData(relatedItems)).apply {
onChipClickedHandler = { tag -> onChipClickedHandler = { tag ->
this@GalleryDialogFragment.onChipClickedHandler.forEach { handler -> this@GalleryDialogFragment.onChipClickedHandler.forEach { handler ->
handler.invoke(tag) handler.invoke(tag)

View File

@@ -25,14 +25,20 @@ import android.view.Window
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.adapters.SourceAdapter import xyz.quaver.pupil.adapters.SourceAdapter
import xyz.quaver.pupil.sources.AnySource
import xyz.quaver.pupil.sources.Source 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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return Dialog(requireContext()).apply { return Dialog(requireContext()).apply {
@@ -41,7 +47,7 @@ class SourceSelectDialog : DialogFragment() {
setContentView(RecyclerView(context).apply { setContentView(RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = SourceAdapter(sources.values.toList()).apply { adapter = SourceAdapter(direct.instance()).apply {
onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener
} }
}) })

View File

@@ -28,14 +28,17 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import okhttp3.* import okhttp3.*
import org.kodein.di.DIAware
import org.kodein.di.android.x.di
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
class ManageFavoritesFragment : PreferenceFragmentCompat() { class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware {
override val di by di()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey) setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey)
@@ -87,7 +90,7 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
.setTitle(R.string.settings_restore_title) .setTitle(R.string.settings_restore_title)
.setView(editText) .setView(editText)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
restore(editText.text.toString(), restore(context, editText.text.toString(),
onFailure = onFailure@{ onFailure = onFailure@{
val view = view ?: return@onFailure val view = view ?: return@onFailure
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show() 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 org.kodein.di.instance
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.DownloadManager
import xyz.quaver.pupil.util.ImageCache
import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.size
class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.OnPreferenceClickListener { class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.OnPreferenceClickListener {
@@ -43,6 +39,8 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
private val downloadManager: DownloadManager by instance() private val downloadManager: DownloadManager by instance()
private val cache: ImageCache by instance() private val cache: ImageCache by instance()
private val histories: SavedSourceSet by instance(tag = "histories")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.manage_storage_preferences, rootKey) 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) setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
histories.clear() 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) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show() }.show()
@@ -169,7 +167,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), DIAware, Preference.On
with (findPreference<Preference>("clear_history")) { with (findPreference<Preference>("clear_history")) {
this ?: return@with 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 onPreferenceClickListener = this@ManageStorageFragment
} }

View File

@@ -21,28 +21,23 @@ package xyz.quaver.pupil.ui.view
import android.content.Context import android.content.Context
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Animatable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R 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.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.* import java.util.*
class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :

View File

@@ -22,15 +22,22 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.android.material.chip.Chip 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.R
import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.sources.Hitomi import xyz.quaver.pupil.sources.Hitomi
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.SavedSourceSet
import xyz.quaver.pupil.util.translations import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
@SuppressLint("ViewConstructor") @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 = val tag: Tag =
_tag.let { _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) setChipBackgroundColorResource(R.color.material_orange_500)
isCloseIconVisible = true isCloseIconVisible = true
closeIcon = ContextCompat.getDrawable(context, closeIcon = ContextCompat.getDrawable(context,
if (favoriteTags.contains(tag)) if (favoriteTags.map[source]?.contains(tag.toString()) == true)
R.drawable.ic_star_filled R.drawable.ic_star_filled
else else
R.drawable.ic_star_empty R.drawable.ic_star_empty
) )
setOnCloseIconClickListener { setOnCloseIconClickListener {
if (favoriteTags.contains(tag)) { if (favoriteTags.map[source]?.contains(tag.toString()) == true) {
favoriteTags.remove(tag) favoriteTags.remove(source, tag.toString())
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty) closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_empty)
when(tag.area) { when(tag.area) {
@@ -78,7 +85,7 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
else -> chipBackgroundColor = null else -> chipBackgroundColor = null
} }
} else { } else {
favoriteTags.add(tag) favoriteTags.add(source, tag.toString())
closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled) closeIcon = ContextCompat.getDrawable(context, R.drawable.ic_star_filled)
setChipBackgroundColorResource(R.color.material_orange_500) 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.Tag
import xyz.quaver.pupil.types.Tags 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 { object Defaults {
const val maxChipSize = 10 const val maxChipSize = 10
@@ -53,7 +53,7 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
for (i in maxChipSize until tags.size) { for (i in maxChipSize until tags.size) {
val tag = tags.elementAt(i) val tag = tags.elementAt(i)
addView(TagChip(context, tag).apply { addView(TagChip(context, source, tag).apply {
setOnClickListener { setOnClickListener {
onClickListener?.invoke(tag) onClickListener?.invoke(tag)
} }
@@ -76,7 +76,7 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
refreshJob = CoroutineScope(Dispatchers.Main).launch { refreshJob = CoroutineScope(Dispatchers.Main).launch {
tags.take(maxChipSize).map { tags.take(maxChipSize).map {
CoroutineScope(Dispatchers.Default).async { CoroutineScope(Dispatchers.Default).async {
TagChip(context, it).apply { TagChip(context, source, it).apply {
setOnClickListener { setOnClickListener {
onClickListener?.invoke(this.tag) onClickListener?.invoke(this.tag)
} }

View File

@@ -29,6 +29,7 @@ import org.kodein.di.android.x.di
import org.kodein.di.instance import org.kodein.di.instance
import xyz.quaver.pupil.sources.AnySource import xyz.quaver.pupil.sources.AnySource
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.util.source
class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware { class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware {
@@ -41,7 +42,7 @@ class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware
val related: LiveData<List<ItemInfo>> = _related val related: LiveData<List<ItemInfo>> = _related
fun load(source: String, itemID: String) { fun load(source: String, itemID: String) {
val source: AnySource by instance(tag = source) val source: AnySource by source(source)
viewModelScope.launch { viewModelScope.launch {
_info.value = withContext(Dispatchers.IO) { _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.sources.AnySource
import xyz.quaver.pupil.util.ImageCache import xyz.quaver.pupil.util.ImageCache
import xyz.quaver.pupil.util.notify import xyz.quaver.pupil.util.notify
import xyz.quaver.pupil.util.source
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware { class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
@@ -53,7 +54,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun load(sourceName: String, itemID: String) { fun load(sourceName: String, itemID: String) {
val source: AnySource by instance(tag = sourceName) val source: AnySource by source(sourceName)
viewModelScope.launch { viewModelScope.launch {
_title.value = withContext(Dispatchers.IO) { _title.value = withContext(Dispatchers.IO) {

View File

@@ -83,7 +83,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D
@Synchronized @Synchronized
fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch { 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 info = async { source.info(itemID) }
val images = async { source.images(itemID) } val images = async { source.images(itemID) }

View File

@@ -23,7 +23,9 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.Json.Default.decodeFromString
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import java.io.File import java.io.File
@@ -44,7 +46,7 @@ class SavedSet <T: Any> (private val file: File, any: T, private val set: Mutabl
fun load() { fun load() {
set.clear() set.clear()
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString(serializer, file.readText()) decodeFromString(serializer, file.readText())
}.onSuccess { }.onSuccess {
set.addAll(it) set.addAll(it)
} }
@@ -111,7 +113,7 @@ class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V,
fun load() { fun load() {
map.clear() map.clear()
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString(serializer, file.readText()) decodeFromString(serializer, file.readText())
}.onSuccess { }.onSuccess {
map.putAll(it) map.putAll(it)
} }
@@ -168,3 +170,78 @@ class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V,
} }
} }
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.annotation.SuppressLint
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.kodein.di.*
import xyz.quaver.hitomi.GalleryInfo import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.SourceEntries
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.* import java.util.*
@@ -129,3 +132,14 @@ fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytes
} }
return bytesCopied 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 package xyz.quaver.pupil.util
import android.content.Context
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString

View File

@@ -18,37 +18,33 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.util.Base64
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response 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 ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL 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)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -201,9 +197,10 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
} }
override fun onResponse(call: Call, response: Response) { 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 { kotlin.runCatching {
Json.decodeFromString<List<String>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let { Json.decodeFromString<Set<String>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let {
favorites.addAll(it) favorites.favorites.addAll(mapOf("hitomi.la" to it))
onSuccess?.invoke(it) onSuccess?.invoke(it)
} }
}.onFailure { onFailure?.invoke(it) } }.onFailure { onFailure?.invoke(it) }

View File

@@ -63,8 +63,8 @@
<com.github.piasy.biv.view.BigImageView <com.github.piasy.biv.view.BigImageView
android:id="@+id/image" android:id="@+id/image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
app:initScaleType="fitCenter" app:initScaleType="centerInside"
app:optimizeDisplay="true" app:optimizeDisplay="true"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"/>

View File

@@ -10,7 +10,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.gms:google-services:4.3.4" classpath "com.google.gms:google-services:4.3.5"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath "com.google.firebase:firebase-crashlytics-gradle:2.4.1" classpath "com.google.firebase:firebase-crashlytics-gradle:2.4.1"
@@ -23,7 +23,9 @@ allprojects {
repositories { repositories {
maven { url "https://dl.bintray.com/piasy/maven" } maven { url "https://dl.bintray.com/piasy/maven" }
google() google()
mavenCentral()
jcenter() jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://guardian.github.com/maven/repo-releases" } maven { url "https://guardian.github.com/maven/repo-releases" }
} }

View File

@@ -21,4 +21,4 @@ android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
android.enableBuildCache=true android.enableBuildCache=true
kotlin_version=1.4.21 kotlin_version=1.4.30