Download tab

This commit is contained in:
tom5079
2021-06-25 14:45:10 +09:00
parent 975b98e4dc
commit 2a92d287af
10 changed files with 123 additions and 59 deletions

View File

@@ -48,7 +48,7 @@ import kotlin.math.roundToInt
data class ReaderItem( data class ReaderItem(
val progress: Float, val progress: Float,
val image: File? val image: Uri?
) )
class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderItemDiffCallback()) { class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderItemDiffCallback()) {
@@ -107,7 +107,7 @@ class ReaderAdapter : ListAdapter<ReaderItem, ReaderAdapter.ViewHolder>(ReaderIt
if (image != null) { if (image != null) {
binding.root.background = null binding.root.background = null
binding.image.showImage(Uri.fromFile(image)) binding.image.showImage(image)
} else { } else {
binding.root.setBackgroundResource(R.drawable.reader_item_boundary) binding.root.setBackgroundResource(R.drawable.reader_item_boundary)

View File

@@ -136,12 +136,12 @@ abstract class Source<Query_SortMode: Enum<Query_SortMode>, Suggestion: SearchSu
typealias SourceEntry = Pair<String, AnySource> typealias SourceEntry = Pair<String, AnySource>
typealias SourceEntries = Set<SourceEntry> typealias SourceEntries = Set<SourceEntry>
typealias PreferenceID = Pair<String, Int> typealias SourcePreferenceID = Pair<String, Int>
typealias PreferenceIDs = Set<PreferenceID> typealias SourcePreferenceIDs = Set<SourcePreferenceID>
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val sourceModule = DI.Module(name = "source") { val sourceModule = DI.Module(name = "source") {
bindSet<SourceEntry>() bindSet<SourceEntry>()
bindSet<PreferenceID>() bindSet<SourcePreferenceID>()
listOf( listOf(
Hitomi() Hitomi()
@@ -151,5 +151,5 @@ val sourceModule = DI.Module(name = "source") {
} }
bind { factory { source: String -> History(di, source) } } bind { factory { source: String -> History(di, source) } }
bind { singleton { Downloads(di) } } inSet { singleton { Downloads(di).let { it.name to (it as AnySource) } } }
} }

View File

@@ -18,11 +18,8 @@
package xyz.quaver.pupil.sources package xyz.quaver.pupil.sources
import android.app.Application import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.kodein.di.DI import org.kodein.di.DI
@@ -39,17 +36,15 @@ import kotlin.math.min
class Downloads(override val di: DI) : Source<DefaultSortMode, SearchSuggestion>(), DIAware { class Downloads(override val di: DI) : Source<DefaultSortMode, SearchSuggestion>(), DIAware {
override val name: String override val name: String
get() = "Downloads" get() = "downloads"
override val iconResID: Int override val iconResID: Int
get() = R.drawable.ic_download get() = R.drawable.ic_download
override val preferenceID: Int override val preferenceID: Int
get() = -1 get() = R.xml.download_preferences
override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values() override val availableSortMode: Array<DefaultSortMode> = DefaultSortMode.values()
private val downloadManager: DownloadManager by instance() private val downloadManager: DownloadManager by instance()
private val applicationContext: Application by instance()
override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> { override suspend fun search(query: String, range: IntRange, sortMode: Enum<*>): Pair<Channel<ItemInfo>, Int> {
val downloads = downloadManager.downloads.toList() val downloads = downloadManager.downloads.toList()
@@ -74,30 +69,38 @@ class Downloads(override val di: DI) : Source<DefaultSortMode, SearchSuggestion>
} }
override suspend fun images(itemID: String): List<String> { override suspend fun images(itemID: String): List<String> {
TODO("Not yet implemented") return downloadManager.downloadFolder.getChild(itemID).let {
if (!it.exists()) null else images(it)
}!!
} }
override suspend fun info(itemID: String): ItemInfo { override suspend fun info(itemID: String): ItemInfo {
TODO("Not yet implemented") return transform(downloadManager.downloadFolder.getChild(itemID))
} }
companion object { companion object {
private fun firstImage(folder: FileX): String? = private fun images(folder: FileX): List<String>? =
folder.list { _, name -> folder.list { _, name ->
name.takeLastWhile { it != '.' } !in listOf("jpg", "png", "gif", "webp") name.takeLastWhile { it != '.' } in listOf("jpg", "png", "gif", "webp")
}?.firstOrNull() }?.toList()
fun transform(folder: FileX): ItemInfo = suspend fun transform(folder: FileX): ItemInfo = withContext(Dispatchers.Unconfined) {
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<ItemInfo>(folder.getChild(".metadata").readText()) Json.decodeFromString<ItemInfo>(folder.getChild(".metadata").readText())
}.getOrNull() ?: }.getOrNull() ?: run {
ItemInfo( val images = images(folder)
"Downloads", ItemInfo(
"", "Downloads",
folder.name, folder.name,
firstImage(folder) ?: "", folder.name,
"" images?.firstOrNull() ?: "",
) "",
mapOf(
ItemInfo.ExtraType.PAGECOUNT to async { images?.size?.toString() }
)
)
}
}
} }
} }

View File

@@ -27,14 +27,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.kodein.di.* import org.kodein.di.*
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.android.x.di
import org.kodein.type.jvmType
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.adapters.SourceAdapter import xyz.quaver.pupil.adapters.SourceAdapter
import xyz.quaver.pupil.sources.AnySource import xyz.quaver.pupil.sources.*
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.SourceEntries
import xyz.quaver.pupil.util.ItemClickSupport
class SourceSelectDialog : DialogFragment(), DIAware { class SourceSelectDialog : DialogFragment(), DIAware {
@@ -48,9 +42,14 @@ class SourceSelectDialog : DialogFragment(), DIAware {
window?.requestFeature(Window.FEATURE_NO_TITLE) window?.requestFeature(Window.FEATURE_NO_TITLE)
window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
val sourcesWithPreferenceID = direct.instance<SourcePreferenceIDs>().map { it.first }
val preferences = direct.instance<SourceEntries>().filter {
it.first in sourcesWithPreferenceID
}.toSet()
setContentView(RecyclerView(context).apply { setContentView(RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = SourceAdapter(direct.instance()).apply { adapter = SourceAdapter(preferences).apply {
onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener onSourceSelectedListener = this@SourceSelectDialog.onSourceSelectedListener
onSourceSettingsSelectedListener = this@SourceSelectDialog.onSourceSettingsSelectedListener onSourceSettingsSelectedListener = this@SourceSelectDialog.onSourceSettingsSelectedListener
} }

View File

@@ -30,7 +30,7 @@ import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.direct import org.kodein.di.direct
import org.kodein.di.instance import org.kodein.di.instance
import xyz.quaver.pupil.sources.PreferenceIDs import xyz.quaver.pupil.sources.SourcePreferenceIDs
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialogFragment import xyz.quaver.pupil.ui.dialog.DefaultQueryDialogFragment
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.getAvailableLanguages import xyz.quaver.pupil.util.getAvailableLanguages
@@ -45,7 +45,7 @@ class SourceSettingsFragment(private val source: String) :
override val di by closestDI() override val di by closestDI()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(direct.instance<PreferenceIDs>().toMap()[source]!!, rootKey) setPreferencesFromResource(direct.instance<SourcePreferenceIDs>().toMap()[source]!!, rootKey)
initPreferences() initPreferences()
} }

View File

@@ -105,13 +105,18 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
fun setModeAndReset(mode: MainMode) { fun setModeAndReset(mode: MainMode) {
sourceFactory = when (mode) { sourceFactory = when (mode) {
MainMode.SEARCH -> defaultSourceFactory MainMode.SEARCH, MainMode.DOWNLOADS -> defaultSourceFactory
MainMode.HISTORY -> { { direct.instance<String, History>(arg = it) } } MainMode.HISTORY -> { { direct.instance<String, History>(arg = it) } }
MainMode.DOWNLOADS -> { { direct.instance<Downloads>() } }
else -> return else -> return
} }
setSourceAndReset(source.value!!.name) setSourceAndReset(
when {
mode == MainMode.DOWNLOADS -> "downloads"
source.value is Downloads -> "hitomi.la"
else -> source.value!!.name
}
)
} }
fun query() { fun query() {

View File

@@ -20,6 +20,7 @@
package xyz.quaver.pupil.ui.viewmodel package xyz.quaver.pupil.ui.viewmodel
import android.app.Application import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -29,8 +30,10 @@ import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.Request import okhttp3.Request
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.android.x.di 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.pupil.adapters.ReaderItem 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
@@ -40,7 +43,7 @@ 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 {
override val di by di() override val di by closestDI()
private val cache: ImageCache by instance() private val cache: ImageCache by instance()
@@ -71,29 +74,38 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
_images.value = images _images.value = images
images.forEachIndexed { index, image -> images.forEachIndexed { index, image ->
val file = cache.load( when (val scheme = image.takeWhile { it != ':' }) {
Request.Builder() "http", "https" -> {
.url(image) val file = cache.load(
.headers(source.getHeadersForImage(itemID, image).toHeaders()) Request.Builder()
.build() .url(image)
) .headers(source.getHeadersForImage(itemID, image).toHeaders())
.build()
)
val channel = cache.channels[image] ?: error("Channel is null") val channel = cache.channels[image] ?: error("Channel is null")
channel.invokeOnClose { e -> channel.invokeOnClose { e ->
viewModelScope.launch { viewModelScope.launch {
if (e == null) { if (e == null) {
_readerItems.value!![index] = ReaderItem(_readerItems.value!![index].progress, file) _readerItems.value!![index] = ReaderItem(_readerItems.value!![index].progress, Uri.fromFile(file))
_readerItems.notify() _readerItems.notify()
}
}
}
launch {
for (progress in channel) {
_readerItems.value!![index] = ReaderItem(progress, _readerItems.value!![index].image)
_readerItems.notify()
}
} }
} }
} "content" -> {
_readerItems.value!![index] = ReaderItem(100f, Uri.parse(image))
launch {
for (progress in channel) {
_readerItems.value!![index] = ReaderItem(progress, _readerItems.value!![index].image)
_readerItems.notify() _readerItems.notify()
} }
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
} }
} }
} }

View File

@@ -104,6 +104,9 @@
android:id="@+id/id_view" android:id="@+id/id_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxWidth="150dp"
android:ellipsize="end"
android:maxLines="1"
android:layout_margin="8dp" android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/divider" app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
</PreferenceScreen>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
</PreferenceScreen>