WIP
This commit is contained in:
5
.idea/jarRepositories.xml
generated
5
.idea/jarRepositories.xml
generated
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
27
app/src/main/java/xyz/quaver/pupil/migrate/Migrate.kt
Normal file
27
app/src/main/java/xyz/quaver/pupil/migrate/Migrate.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.migrate
|
||||||
|
|
||||||
|
class Migrate {
|
||||||
|
|
||||||
|
fun migrate() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
25
app/src/main/java/xyz/quaver/pupil/migrate/Migrate001.kt
Normal file
25
app/src/main/java/xyz/quaver/pupil/migrate/Migrate001.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.migrate
|
||||||
|
|
||||||
|
class Migrate001 {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,10 +29,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.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) }
|
||||||
}
|
}
|
||||||
72
app/src/main/java/xyz/quaver/pupil/sources/History.kt
Normal file
72
app/src/main/java/xyz/quaver/pupil/sources/History.kt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.orhanobut.logger.Logger
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.di
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
|
import xyz.quaver.pupil.util.SavedSourceSet
|
||||||
|
import xyz.quaver.pupil.util.source
|
||||||
|
|
||||||
|
class History(override val di: DI, source: String) : Source<DefaultSortMode, SearchSuggestion>(), DIAware {
|
||||||
|
|
||||||
|
private val source: AnySource by source(source)
|
||||||
|
private val histories: SavedSourceSet by instance(tag = "histories")
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = source.name
|
||||||
|
override val iconResID: Int
|
||||||
|
get() = source.iconResID
|
||||||
|
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
|
||||||
|
|
||||||
|
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> {
|
||||||
|
val channel = Channel<ItemInfo>()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
Logger.d(histories.map)
|
||||||
|
histories.map[source.name]?.forEach {
|
||||||
|
channel.send(source.info(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(channel, histories.map.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
||||||
|
return source.suggestion(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun images(itemID: String): List<String> {
|
||||||
|
return source.images(itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun info(itemID: String): ItemInfo {
|
||||||
|
return source.info(itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ class Hiyobi : Source<DefaultSortMode, DefaultSearchSuggestion>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun images(itemID: String): List<String> {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) :
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
174
app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
Normal file
174
app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.x.di
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
|
import xyz.quaver.pupil.sources.AnySource
|
||||||
|
import xyz.quaver.pupil.sources.ItemInfo
|
||||||
|
import xyz.quaver.pupil.util.Preferences
|
||||||
|
import xyz.quaver.pupil.util.notify
|
||||||
|
import xyz.quaver.pupil.util.source
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||||
|
|
||||||
|
override val di by di()
|
||||||
|
|
||||||
|
private val _searchResults = MutableLiveData<MutableList<ItemInfo>>()
|
||||||
|
val searchResults = _searchResults as LiveData<List<ItemInfo>>
|
||||||
|
var loading = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var queryJob: Job? = null
|
||||||
|
private var suggestionJob: Job? = null
|
||||||
|
|
||||||
|
val query = MutableLiveData<String>()
|
||||||
|
private val queryStack = mutableListOf<String>()
|
||||||
|
|
||||||
|
private val _source = MutableLiveData<AnySource>()
|
||||||
|
val source: LiveData<AnySource> = _source
|
||||||
|
|
||||||
|
val availableSortMode = Transformations.map(_source) {
|
||||||
|
it.availableSortMode
|
||||||
|
}
|
||||||
|
val sortMode = MutableLiveData<Enum<*>>()
|
||||||
|
|
||||||
|
val sourceIcon = Transformations.map(_source) {
|
||||||
|
it.iconResID
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceName = Transformations.map(_source) {
|
||||||
|
it.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _currentPage = MutableLiveData<Int>()
|
||||||
|
val currentPage: LiveData<Int> = _currentPage
|
||||||
|
|
||||||
|
private val totalItems = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val totalPages = Transformations.map(totalItems) {
|
||||||
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
|
|
||||||
|
ceil(it / perPage.toDouble()).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _suggestions = MutableLiveData<List<SearchSuggestion>>()
|
||||||
|
val suggestions: LiveData<List<SearchSuggestion>> = _suggestions
|
||||||
|
|
||||||
|
init {
|
||||||
|
setSourceAndReset("hitomi.la")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSourceAndReset(sourceName: String) {
|
||||||
|
_source.value = direct.source(sourceName).also {
|
||||||
|
sortMode.value = it.availableSortMode.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
setQueryAndSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setQueryAndSearch(query: String = "") {
|
||||||
|
this.query.value = query
|
||||||
|
setPage(1)
|
||||||
|
|
||||||
|
query()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun query() {
|
||||||
|
val perPage = Preferences["per_page", "25"].toInt()
|
||||||
|
val source = _source.value ?: error("Source is null")
|
||||||
|
val sortMode = sortMode.value ?: source.availableSortMode.first()
|
||||||
|
val currentPage = currentPage.value ?: 1
|
||||||
|
|
||||||
|
suggestionJob?.cancel()
|
||||||
|
queryJob?.cancel()
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
val results = mutableListOf<ItemInfo>()
|
||||||
|
_searchResults.value = results
|
||||||
|
|
||||||
|
queryJob = viewModelScope.launch {
|
||||||
|
val channel = withContext(Dispatchers.IO) {
|
||||||
|
val (channel, count) = source.search(
|
||||||
|
(query.value ?: "") + Preferences["default_query", ""],
|
||||||
|
(currentPage - 1) * perPage until currentPage * perPage,
|
||||||
|
sortMode
|
||||||
|
)
|
||||||
|
|
||||||
|
totalItems.postValue(count)
|
||||||
|
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
|
||||||
|
for (result in channel) {
|
||||||
|
yield()
|
||||||
|
results.add(result)
|
||||||
|
_searchResults.notify()
|
||||||
|
}
|
||||||
|
_searchResults.notify()
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prevPage() { _currentPage.value = _currentPage.value!! - 1 }
|
||||||
|
fun nextPage() { _currentPage.value = _currentPage.value!! + 1 }
|
||||||
|
fun setPage(page: Int) { _currentPage.value = page }
|
||||||
|
|
||||||
|
fun random(callback: (ItemInfo) -> Unit) {
|
||||||
|
if (totalItems.value!! == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
val random = Random.Default.nextInt(totalItems.value!!)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
_source.value?.search(
|
||||||
|
query.value + Preferences["default_query", ""],
|
||||||
|
random .. random,
|
||||||
|
sortMode.value!!
|
||||||
|
)?.first?.receive()
|
||||||
|
}?.let(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun suggestion() {
|
||||||
|
suggestionJob?.cancel()
|
||||||
|
|
||||||
|
_suggestions.value = mutableListOf()
|
||||||
|
|
||||||
|
suggestionJob = viewModelScope.launch {
|
||||||
|
_suggestions.value = withContext(Dispatchers.IO) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
_source.value!!.suggestion(query.value!!)
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
}!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import xyz.quaver.pupil.adapters.ReaderItem
|
|||||||
import xyz.quaver.pupil.sources.AnySource
|
import xyz.quaver.pupil.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) {
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user