Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
74569615e3
|
|||
|
f3c320a90f
|
|||
|
|
a3012ab458 | ||
|
|
6ec58879fd | ||
|
571cf08c53
|
|||
|
fca53eee7a
|
|||
| ed9e2eb4d2 | |||
|
c0e94f8415
|
|||
|
e172d619a1
|
|||
|
d6c64fc638
|
|||
|
37404cb9a6
|
|||
|
9d5271ff26
|
|||
|
5f59432e48
|
|||
|
5c082b5cdb
|
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 587
|
versionCode = 589
|
||||||
versionName = '6.2'
|
versionName = '6.2.2'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||||
ksp {
|
ksp {
|
||||||
@@ -81,7 +81,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a61e441e79') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:0054d06e6e') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
|
|||||||
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
|
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
|
||||||
|
|
||||||
class GZipInterceptor : Interceptor {
|
class GZipInterceptor : Interceptor {
|
||||||
@@ -9,6 +10,10 @@ class GZipInterceptor : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val newRequest = chain.request().newBuilder()
|
val newRequest = chain.request().newBuilder()
|
||||||
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
||||||
return chain.proceed(newRequest.build())
|
return try {
|
||||||
|
chain.proceed(newRequest.build())
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ class RemoteMangaRepository(
|
|||||||
return details.await()
|
return details.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
|
return cache.getDetails(source, manga.url)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun find(manga: Manga): Manga? {
|
suspend fun find(manga: Manga): Manga? {
|
||||||
val list = getList(0, manga.title)
|
val list = getList(0, manga.title)
|
||||||
return list.find { x -> x.id == manga.id }
|
return list.find { x -> x.id == manga.id }
|
||||||
|
|||||||
@@ -126,10 +126,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
ColorUtils.compositeColors(
|
ColorUtils.compositeColors(
|
||||||
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
|
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
|
||||||
getThemeColor(com.google.android.material.R.attr.colorSurface),
|
getThemeColor(R.attr.m3ColorBackground),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ContextCompat.getColor(this, R.color.kotatsu_secondaryContainer)
|
ContextCompat.getColor(this, R.color.kotatsu_m3_background)
|
||||||
}
|
}
|
||||||
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
|
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
|
||||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.onEachWhile(action: suspend (T) -> Boolean): Flow<T> {
|
||||||
|
var isCalled = false
|
||||||
|
return onEach {
|
||||||
|
if (!isCalled) {
|
||||||
|
isCalled = action(it)
|
||||||
|
}
|
||||||
|
}.onCompletion {
|
||||||
|
isCalled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
|
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
|
||||||
return map { list -> list.map(transform) }
|
return map { list -> list.map(transform) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,4 +86,6 @@ class DetailsInteractor @Inject constructor(
|
|||||||
subject
|
subject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findLocal(seed: Manga) = localMangaRepository.getRemoteManga(seed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,9 @@ class DetailsActivity :
|
|||||||
this,
|
this,
|
||||||
MenuInvalidator(viewBinding.toolbarChapters ?: this),
|
MenuInvalidator(viewBinding.toolbarChapters ?: this),
|
||||||
)
|
)
|
||||||
viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
|
val menuInvalidator = MenuInvalidator(this)
|
||||||
|
viewModel.favouriteCategories.observe(this, menuInvalidator)
|
||||||
|
viewModel.remoteManga.observe(this, menuInvalidator)
|
||||||
viewModel.branches.observe(this) {
|
viewModel.branches.observe(this) {
|
||||||
viewBinding.buttonDropdown.isVisible = it.size > 1
|
viewBinding.buttonDropdown.isVisible = it.size > 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class DetailsFragment :
|
|||||||
BaseFragment<FragmentDetailsBinding>(),
|
BaseFragment<FragmentDetailsBinding>(),
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
ChipsView.OnChipClickListener,
|
ChipsView.OnChipClickListener,
|
||||||
OnListItemClickListener<Bookmark>, ViewTreeObserver.OnDrawListener {
|
OnListItemClickListener<Bookmark>, ViewTreeObserver.OnDrawListener, View.OnLayoutChangeListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
@@ -105,6 +105,7 @@ class DetailsFragment :
|
|||||||
binding.buttonScrobblingMore.setOnClickListener(this)
|
binding.buttonScrobblingMore.setOnClickListener(this)
|
||||||
binding.buttonRelatedMore.setOnClickListener(this)
|
binding.buttonRelatedMore.setOnClickListener(this)
|
||||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||||
|
binding.textViewDescription.addOnLayoutChangeListener(this)
|
||||||
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||||
binding.chipsTags.onChipClickListener = this
|
binding.chipsTags.onChipClickListener = this
|
||||||
@@ -150,6 +151,22 @@ class DetailsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLayoutChange(
|
||||||
|
v: View?,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int,
|
||||||
|
oldLeft: Int,
|
||||||
|
oldTop: Int,
|
||||||
|
oldRight: Int,
|
||||||
|
oldBottom: Int
|
||||||
|
) {
|
||||||
|
with(viewBinding ?: return) {
|
||||||
|
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMangaUpdated(manga: Manga) {
|
private fun onMangaUpdated(manga: Manga) {
|
||||||
with(requireViewBinding()) {
|
with(requireViewBinding()) {
|
||||||
// Main
|
// Main
|
||||||
@@ -228,7 +245,6 @@ class DetailsFragment :
|
|||||||
} else {
|
} else {
|
||||||
tv.text = description
|
tv.text = description
|
||||||
}
|
}
|
||||||
requireViewBinding().buttonDescriptionMore.isVisible = tv.isTextTruncated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLocalSizeChanged(size: Long) {
|
private fun onLocalSizeChanged(size: Long) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class DetailsMenuProvider(
|
|||||||
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
|
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
|
||||||
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
|
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
|
||||||
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
|
||||||
|
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
|
||||||
menu.findItem(R.id.action_favourite).setIcon(
|
menu.findItem(R.id.action_favourite).setIcon(
|
||||||
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
|
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
|
||||||
)
|
)
|
||||||
@@ -88,6 +89,12 @@ class DetailsMenuProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_online -> {
|
||||||
|
viewModel.remoteManga.value?.let {
|
||||||
|
activity.startActivity(DetailsActivity.newIntent(activity, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
R.id.action_related -> {
|
R.id.action_related -> {
|
||||||
viewModel.manga.value?.let {
|
viewModel.manga.value?.let {
|
||||||
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
|
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
|
|||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onEachWhile
|
||||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||||
import org.koitharu.kotatsu.details.data.MangaDetails
|
import org.koitharu.kotatsu.details.data.MangaDetails
|
||||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||||
@@ -94,6 +94,8 @@ class DetailsViewModel @Inject constructor(
|
|||||||
val favouriteCategories = interactor.observeIsFavourite(mangaId)
|
val favouriteCategories = interactor.observeIsFavourite(mangaId)
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
|
val remoteManga = MutableStateFlow<Manga?>(null)
|
||||||
|
|
||||||
val newChaptersCount = details.flatMapLatest { d ->
|
val newChaptersCount = details.flatMapLatest { d ->
|
||||||
if (d?.isLocal == false) {
|
if (d?.isLocal == false) {
|
||||||
interactor.observeNewChapters(mangaId)
|
interactor.observeNewChapters(mangaId)
|
||||||
@@ -213,6 +215,10 @@ class DetailsViewModel @Inject constructor(
|
|||||||
progressUpdateUseCase(manga.toManga())
|
progressUpdateUseCase(manga.toManga())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob
|
||||||
|
remoteManga.value = interactor.findLocal(manga.toManga())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
@@ -313,11 +319,15 @@ class DetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||||
detailsLoadUseCase.invoke(intent)
|
detailsLoadUseCase.invoke(intent)
|
||||||
.onFirst {
|
.onEachWhile {
|
||||||
|
if (it.allChapters.isEmpty()) {
|
||||||
|
return@onEachWhile false
|
||||||
|
}
|
||||||
val manga = it.toManga()
|
val manga = it.toManga()
|
||||||
// find default branch
|
// find default branch
|
||||||
val hist = historyRepository.getOne(manga)
|
val hist = historyRepository.getOne(manga)
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||||
|
true
|
||||||
}.collect {
|
}.collect {
|
||||||
details.value = it
|
details.value = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ data class DownloadState(
|
|||||||
val eta: Long = -1L,
|
val eta: Long = -1L,
|
||||||
val localManga: LocalManga? = null,
|
val localManga: LocalManga? = null,
|
||||||
val downloadedChapters: LongArray = LongArray(0),
|
val downloadedChapters: LongArray = LongArray(0),
|
||||||
|
val scheduledChapters: LongArray = LongArray(0),
|
||||||
val timestamp: Long = System.currentTimeMillis(),
|
val timestamp: Long = System.currentTimeMillis(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ data class DownloadState(
|
|||||||
.putLong(DATA_TIMESTAMP, timestamp)
|
.putLong(DATA_TIMESTAMP, timestamp)
|
||||||
.putString(DATA_ERROR, error)
|
.putString(DATA_ERROR, error)
|
||||||
.putLongArray(DATA_CHAPTERS, downloadedChapters)
|
.putLongArray(DATA_CHAPTERS, downloadedChapters)
|
||||||
|
.putLongArray(DATA_CHAPTERS_SRC, scheduledChapters)
|
||||||
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
|
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
|
||||||
.putBoolean(DATA_PAUSED, isPaused)
|
.putBoolean(DATA_PAUSED, isPaused)
|
||||||
.build()
|
.build()
|
||||||
@@ -64,10 +66,13 @@ data class DownloadState(
|
|||||||
if (eta != other.eta) return false
|
if (eta != other.eta) return false
|
||||||
if (localManga != other.localManga) return false
|
if (localManga != other.localManga) return false
|
||||||
if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false
|
if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false
|
||||||
|
if (!scheduledChapters.contentEquals(other.scheduledChapters)) return false
|
||||||
if (timestamp != other.timestamp) return false
|
if (timestamp != other.timestamp) return false
|
||||||
if (max != other.max) return false
|
if (max != other.max) return false
|
||||||
if (progress != other.progress) return false
|
if (progress != other.progress) return false
|
||||||
return percent == other.percent
|
if (percent != other.percent) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -83,6 +88,7 @@ data class DownloadState(
|
|||||||
result = 31 * result + eta.hashCode()
|
result = 31 * result + eta.hashCode()
|
||||||
result = 31 * result + (localManga?.hashCode() ?: 0)
|
result = 31 * result + (localManga?.hashCode() ?: 0)
|
||||||
result = 31 * result + downloadedChapters.contentHashCode()
|
result = 31 * result + downloadedChapters.contentHashCode()
|
||||||
|
result = 31 * result + scheduledChapters.contentHashCode()
|
||||||
result = 31 * result + timestamp.hashCode()
|
result = 31 * result + timestamp.hashCode()
|
||||||
result = 31 * result + max
|
result = 31 * result + max
|
||||||
result = 31 * result + progress
|
result = 31 * result + progress
|
||||||
@@ -90,12 +96,14 @@ data class DownloadState(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DATA_MANGA_ID = "manga_id"
|
private const val DATA_MANGA_ID = "manga_id"
|
||||||
private const val DATA_MAX = "max"
|
private const val DATA_MAX = "max"
|
||||||
private const val DATA_PROGRESS = "progress"
|
private const val DATA_PROGRESS = "progress"
|
||||||
private const val DATA_CHAPTERS = "chapter"
|
private const val DATA_CHAPTERS = "chapter"
|
||||||
|
private const val DATA_CHAPTERS_SRC = "chapters_src"
|
||||||
private const val DATA_ETA = "eta"
|
private const val DATA_ETA = "eta"
|
||||||
private const val DATA_TIMESTAMP = "timestamp"
|
private const val DATA_TIMESTAMP = "timestamp"
|
||||||
private const val DATA_ERROR = "error"
|
private const val DATA_ERROR = "error"
|
||||||
@@ -119,5 +127,7 @@ data class DownloadState(
|
|||||||
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
|
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
|
||||||
|
|
||||||
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
|
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
|
||||||
|
|
||||||
|
fun getScheduledChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS_SRC) ?: LongArray(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
package org.koitharu.kotatsu.download.ui.list
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import android.transition.TransitionManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.drawableEnd
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
||||||
|
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
|
||||||
|
import org.koitharu.kotatsu.download.ui.list.chapters.downloadChapterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.util.format
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
|
|
||||||
@@ -25,6 +36,9 @@ fun downloadItemAD(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
||||||
|
val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
|
||||||
|
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
|
||||||
|
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
|
||||||
|
|
||||||
val clickListener = object : View.OnClickListener, View.OnLongClickListener {
|
val clickListener = object : View.OnClickListener, View.OnLongClickListener {
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
@@ -45,8 +59,13 @@ fun downloadItemAD(
|
|||||||
binding.buttonResume.setOnClickListener(clickListener)
|
binding.buttonResume.setOnClickListener(clickListener)
|
||||||
itemView.setOnClickListener(clickListener)
|
itemView.setOnClickListener(clickListener)
|
||||||
itemView.setOnLongClickListener(clickListener)
|
itemView.setOnLongClickListener(clickListener)
|
||||||
|
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
|
||||||
|
binding.recyclerViewChapters.adapter = chaptersAdapter
|
||||||
|
|
||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
|
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
|
||||||
|
TransitionManager.beginDelayedTransition(binding.constraintLayout)
|
||||||
|
}
|
||||||
binding.textViewTitle.text = item.manga.title
|
binding.textViewTitle.text = item.manga.title
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
@@ -57,6 +76,10 @@ fun downloadItemAD(
|
|||||||
source(item.manga.source)
|
source(item.manga.source)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
}
|
}
|
||||||
|
binding.textViewTitle.isChecked = item.isExpanded
|
||||||
|
binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
|
||||||
|
binding.cardDetails.isVisible = item.isExpanded
|
||||||
|
chaptersAdapter.items = item.chapters
|
||||||
when (item.workState) {
|
when (item.workState) {
|
||||||
WorkInfo.State.ENQUEUED,
|
WorkInfo.State.ENQUEUED,
|
||||||
WorkInfo.State.BLOCKED -> {
|
WorkInfo.State.BLOCKED -> {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.download.ui.list
|
|||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
|
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
|
||||||
|
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -19,6 +21,8 @@ data class DownloadItemModel(
|
|||||||
val progress: Int,
|
val progress: Int,
|
||||||
val eta: Long,
|
val eta: Long,
|
||||||
val timestamp: Date,
|
val timestamp: Date,
|
||||||
|
val chapters: List<DownloadChapter>,
|
||||||
|
val isExpanded: Boolean,
|
||||||
) : ListModel, Comparable<DownloadItemModel> {
|
) : ListModel, Comparable<DownloadItemModel> {
|
||||||
|
|
||||||
val percent: Float
|
val percent: Float
|
||||||
@@ -33,6 +37,9 @@ data class DownloadItemModel(
|
|||||||
val canResume: Boolean
|
val canResume: Boolean
|
||||||
get() = workState == WorkInfo.State.RUNNING && isPaused
|
get() = workState == WorkInfo.State.RUNNING && isPaused
|
||||||
|
|
||||||
|
val isExpandable: Boolean
|
||||||
|
get() = chapters.isNotEmpty()
|
||||||
|
|
||||||
fun getEtaString(): CharSequence? = if (hasEta) {
|
fun getEtaString(): CharSequence? = if (hasEta) {
|
||||||
DateUtils.getRelativeTimeSpanString(
|
DateUtils.getRelativeTimeSpanString(
|
||||||
eta,
|
eta,
|
||||||
@@ -51,17 +58,10 @@ data class DownloadItemModel(
|
|||||||
return other is DownloadItemModel && other.id == id
|
return other is DownloadItemModel && other.id == id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(previousState: ListModel): Any? {
|
override fun getChangePayload(previousState: ListModel): Any? = when {
|
||||||
return when (previousState) {
|
previousState !is DownloadItemModel -> super.getChangePayload(previousState)
|
||||||
is DownloadItemModel -> {
|
workState != previousState.workState -> null
|
||||||
if (workState == previousState.workState) {
|
isExpanded != previousState.isExpanded -> ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||||
Unit
|
else -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.getChangePayload(previousState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
|||||||
if (selectionController.onItemClick(item.id.mostSignificantBits)) {
|
if (selectionController.onItemClick(item.id.mostSignificantBits)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startActivity(DetailsActivity.newIntent(view.context, item.manga))
|
if (item.isExpandable) {
|
||||||
|
viewModel.expandCollapse(item)
|
||||||
|
} else {
|
||||||
|
startActivity(DetailsActivity.newIntent(view.context, item.manga))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {
|
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {
|
||||||
|
|||||||
@@ -8,15 +8,19 @@ import androidx.work.Data
|
|||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
@@ -24,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
|||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.daysDiff
|
import org.koitharu.kotatsu.core.util.ext.daysDiff
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
|
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
@@ -31,6 +36,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
|||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -41,13 +47,18 @@ import javax.inject.Inject
|
|||||||
class DownloadsViewModel @Inject constructor(
|
class DownloadsViewModel @Inject constructor(
|
||||||
private val workScheduler: DownloadWorker.Scheduler,
|
private val workScheduler: DownloadWorker.Scheduler,
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val mangaCache = LongSparseArray<Manga>()
|
private val mangaCache = LongSparseArray<Manga>()
|
||||||
private val cacheMutex = Mutex()
|
private val cacheMutex = Mutex()
|
||||||
private val works = workScheduler.observeWorks()
|
private val expanded = MutableStateFlow(emptySet<UUID>())
|
||||||
.mapLatest { it.toDownloadsList() }
|
private val works = combine(
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
workScheduler.observeWorks(),
|
||||||
|
expanded,
|
||||||
|
) { list, exp ->
|
||||||
|
list.toDownloadsList(exp)
|
||||||
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
|
||||||
@@ -169,11 +180,21 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
it.id.mostSignificantBits
|
it.id.mostSignificantBits
|
||||||
} ?: emptySet()
|
} ?: emptySet()
|
||||||
|
|
||||||
private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> {
|
fun expandCollapse(item: DownloadItemModel) {
|
||||||
|
expanded.update {
|
||||||
|
if (item.id in it) {
|
||||||
|
it - item.id
|
||||||
|
} else {
|
||||||
|
it + item.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun List<WorkInfo>.toDownloadsList(exp: Set<UUID>): List<DownloadItemModel> {
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
val list = mapNotNullTo(ArrayList(size)) { it.toUiModel() }
|
val list = mapNotNullTo(ArrayList(size)) { it.toUiModel(it.id in exp) }
|
||||||
list.sortByDescending { it.timestamp }
|
list.sortByDescending { it.timestamp }
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
@@ -213,11 +234,13 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun WorkInfo.toUiModel(): DownloadItemModel? {
|
private suspend fun WorkInfo.toUiModel(isExpanded: Boolean): DownloadItemModel? {
|
||||||
val workData = if (outputData == Data.EMPTY) progress else outputData
|
val workData = if (outputData == Data.EMPTY) progress else outputData
|
||||||
val mangaId = DownloadState.getMangaId(workData)
|
val mangaId = DownloadState.getMangaId(workData)
|
||||||
if (mangaId == 0L) return null
|
if (mangaId == 0L) return null
|
||||||
val manga = getManga(mangaId) ?: return null
|
val manga = getManga(mangaId) ?: return null
|
||||||
|
val downloadedChapters = DownloadState.getDownloadedChapters(workData)
|
||||||
|
val scheduledChapters = DownloadState.getScheduledChapters(workData).toSet()
|
||||||
return DownloadItemModel(
|
return DownloadItemModel(
|
||||||
id = id,
|
id = id,
|
||||||
workState = state,
|
workState = state,
|
||||||
@@ -229,7 +252,19 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
progress = DownloadState.getProgress(workData),
|
progress = DownloadState.getProgress(workData),
|
||||||
eta = DownloadState.getEta(workData),
|
eta = DownloadState.getEta(workData),
|
||||||
timestamp = DownloadState.getTimestamp(workData),
|
timestamp = DownloadState.getTimestamp(workData),
|
||||||
totalChapters = DownloadState.getDownloadedChapters(workData).size,
|
totalChapters = downloadedChapters.size,
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
chapters = manga.chapters?.mapNotNull {
|
||||||
|
if (it.id in scheduledChapters) {
|
||||||
|
DownloadChapter(
|
||||||
|
number = it.number,
|
||||||
|
name = it.name,
|
||||||
|
isDownloaded = it.id in downloadedChapters,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.orEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +296,16 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
return cacheMutex.withLock {
|
return cacheMutex.withLock {
|
||||||
mangaCache.getOrElse(mangaId) {
|
mangaCache.getOrElse(mangaId) {
|
||||||
mangaDataRepository.findMangaById(mangaId)?.also { mangaCache[mangaId] = it } ?: return null
|
mangaDataRepository.findMangaById(mangaId)?.let {
|
||||||
|
tryLoad(it) ?: it
|
||||||
|
}?.also {
|
||||||
|
mangaCache[mangaId] = it
|
||||||
|
} ?: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
|
||||||
|
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).peekDetails(manga)
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list.chapters
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
data class DownloadChapter(
|
||||||
|
val number: Int,
|
||||||
|
val name: String,
|
||||||
|
val isDownloaded: Boolean,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is DownloadChapter && other.name == name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list.chapters
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.drawableEnd
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemChapterDownloadBinding
|
||||||
|
|
||||||
|
fun downloadChapterAD() = adapterDelegateViewBinding<DownloadChapter, DownloadChapter, ItemChapterDownloadBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemChapterDownloadBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val iconDone = ContextCompat.getDrawable(context, R.drawable.ic_check)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewNumber.text = item.number.toString()
|
||||||
|
binding.textViewTitle.text = item.name
|
||||||
|
binding.textViewTitle.drawableEnd = if (item.isDownloaded) iconDone else null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,6 +178,9 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val chapters = getChapters(mangaDetails, includedIds)
|
val chapters = getChapters(mangaDetails, includedIds)
|
||||||
|
publishState(
|
||||||
|
currentState.copy(scheduledChapters = LongArray(chapters.size) { i -> chapters[i].id }),
|
||||||
|
)
|
||||||
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
||||||
if (chaptersToSkip.remove(chapter.id)) {
|
if (chaptersToSkip.remove(chapter.id)) {
|
||||||
publishState(
|
publishState(
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ enum class ListItemType {
|
|||||||
CATEGORY_LARGE,
|
CATEGORY_LARGE,
|
||||||
MANGA_SCROBBLING,
|
MANGA_SCROBBLING,
|
||||||
NAV_ITEM,
|
NAV_ITEM,
|
||||||
|
CHAPTER,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class TypedListSpacingDecoration(
|
|||||||
ListItemType.MANGA_NESTED_GROUP,
|
ListItemType.MANGA_NESTED_GROUP,
|
||||||
ListItemType.CATEGORY_LARGE,
|
ListItemType.CATEGORY_LARGE,
|
||||||
ListItemType.NAV_ITEM,
|
ListItemType.NAV_ITEM,
|
||||||
|
ListItemType.CHAPTER,
|
||||||
null,
|
null,
|
||||||
-> outRect.set(0)
|
-> outRect.set(0)
|
||||||
|
|
||||||
@@ -77,6 +78,6 @@ class TypedListSpacingDecoration(
|
|||||||
private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing)
|
private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing)
|
||||||
|
|
||||||
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|
||||||
|| this == ListItemType.FILTER_SORT
|
|| this == ListItemType.FILTER_SORT
|
||||||
|| this == ListItemType.FILTER_TAG
|
|| this == ListItemType.FILTER_TAG
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
val mangaUri = root.toUri().toString()
|
val mangaUri = root.toUri().toString()
|
||||||
val chapterFiles = getChaptersFiles()
|
val chapterFiles = getChaptersFiles()
|
||||||
val info = index?.getMangaInfo()
|
val info = index?.getMangaInfo()
|
||||||
|
val cover = fileUri(
|
||||||
|
root,
|
||||||
|
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
|
||||||
|
)
|
||||||
val manga = info?.copy2(
|
val manga = info?.copy2(
|
||||||
source = MangaSource.LOCAL,
|
source = MangaSource.LOCAL,
|
||||||
url = mangaUri,
|
url = mangaUri,
|
||||||
coverUrl = fileUri(
|
coverUrl = cover,
|
||||||
root,
|
largeCoverUrl = cover,
|
||||||
index.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
|
|
||||||
),
|
|
||||||
chapters = info.chapters?.mapIndexed { i, c ->
|
chapters = info.chapters?.mapIndexed { i, c ->
|
||||||
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
|
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,10 +67,11 @@ sealed class LocalMangaInput(
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
protected fun Manga.copy2(
|
protected fun Manga.copy2(
|
||||||
url: String = this.url,
|
url: String,
|
||||||
coverUrl: String = this.coverUrl,
|
coverUrl: String,
|
||||||
chapters: List<MangaChapter>? = this.chapters,
|
largeCoverUrl: String,
|
||||||
source: MangaSource = this.source,
|
chapters: List<MangaChapter>?,
|
||||||
|
source: MangaSource,
|
||||||
) = Manga(
|
) = Manga(
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
@@ -91,8 +92,8 @@ sealed class LocalMangaInput(
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
protected fun MangaChapter.copy(
|
protected fun MangaChapter.copy(
|
||||||
url: String = this.url,
|
url: String,
|
||||||
source: MangaSource = this.source,
|
source: MangaSource,
|
||||||
) = MangaChapter(
|
) = MangaChapter(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
|
|||||||
@@ -41,14 +41,15 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
|||||||
val index = entry?.let(zip::readText)?.let(::MangaIndex)
|
val index = entry?.let(zip::readText)?.let(::MangaIndex)
|
||||||
val info = index?.getMangaInfo()
|
val info = index?.getMangaInfo()
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
|
val cover = zipUri(
|
||||||
|
root,
|
||||||
|
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
|
||||||
|
)
|
||||||
return@use info.copy2(
|
return@use info.copy2(
|
||||||
source = MangaSource.LOCAL,
|
source = MangaSource.LOCAL,
|
||||||
url = fileUri,
|
url = fileUri,
|
||||||
coverUrl = zipUri(
|
coverUrl = cover,
|
||||||
root,
|
largeCoverUrl = cover,
|
||||||
entryName = index.getCoverEntry()
|
|
||||||
?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
|
|
||||||
),
|
|
||||||
chapters = info.chapters?.map { c ->
|
chapters = info.chapters?.map { c ->
|
||||||
c.copy(url = fileUri, source = MangaSource.LOCAL)
|
c.copy(url = fileUri, source = MangaSource.LOCAL)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:alpha="0.7" android:color="?attr/m3ColorBackground" />
|
<item android:alpha="0.7" android:color="?attr/m3ColorBottomMenuBackground" />
|
||||||
</selector>
|
</selector>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:scrollIndicators="top"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
|||||||
42
app/src/main/res/layout/item_chapter_download.xml
Normal file
42
app/src/main/res/layout/item_chapter_download.xml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_number"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:background="@drawable/bg_badge_default"
|
||||||
|
android:ellipsize="none"
|
||||||
|
android:gravity="center"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="?attr/colorOnPrimary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="13" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
app:drawableTint="?colorControlNormal"
|
||||||
|
tools:drawableEnd="@drawable/ic_check"
|
||||||
|
tools:text="@tools:sample/lorem[15]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/constraintLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingBottom="12dp">
|
android:paddingBottom="12dp">
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
<TextView
|
<CheckedTextView
|
||||||
android:id="@+id/textView_title"
|
android:id="@+id/textView_title"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -32,11 +33,14 @@
|
|||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
app:drawableTint="?android:colorControlNormal"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:drawableEndCompat="@drawable/ic_expand_collapse"
|
||||||
tools:text="@tools:sample/lorem" />
|
tools:text="@tools:sample/lorem" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -100,6 +104,31 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/textView_status"
|
app:layout_constraintTop_toBottomOf="@id/textView_status"
|
||||||
tools:text="@tools:sample/lorem[3]" />
|
tools:text="@tools:sample/lorem[3]" />
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_details"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHeight_default="wrap"
|
||||||
|
app:layout_constraintHeight_max="280dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
|
app:shapeAppearance="?shapeAppearanceCornerMedium">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerView_chapters"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="200"
|
||||||
|
tools:listitem="@layout/item_chapter_download" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/button_pause"
|
android:id="@+id/button_pause"
|
||||||
style="?materialButtonOutlinedStyle"
|
style="?materialButtonOutlinedStyle"
|
||||||
@@ -110,7 +139,7 @@
|
|||||||
android:text="@string/pause"
|
android:text="@string/pause"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_resume"
|
app:layout_constraintEnd_toStartOf="@id/button_resume"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
app:layout_constraintTop_toBottomOf="@id/card_details"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -123,7 +152,7 @@
|
|||||||
android:text="@string/resume"
|
android:text="@string/resume"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
app:layout_constraintTop_toBottomOf="@id/card_details" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/button_cancel"
|
android:id="@+id/button_cancel"
|
||||||
@@ -135,7 +164,7 @@
|
|||||||
android:text="@android:string/cancel"
|
android:text="@android:string/cancel"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
app:layout_constraintTop_toBottomOf="@id/card_details"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
android:title="@string/find_similar"
|
android:title="@string/find_similar"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_online"
|
||||||
|
android:orderInCategory="50"
|
||||||
|
android:title="@string/online_variant"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_browser"
|
android:id="@+id/action_browser"
|
||||||
android:orderInCategory="50"
|
android:orderInCategory="50"
|
||||||
|
|||||||
@@ -487,10 +487,11 @@
|
|||||||
<string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string>
|
<string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string>
|
||||||
<string name="state_abandoned">Кінута</string>
|
<string name="state_abandoned">Кінута</string>
|
||||||
<string name="categories">Катэгорыі</string>
|
<string name="categories">Катэгорыі</string>
|
||||||
<string name="list_options">Спіс опцый</string>
|
<string name="list_options">Параметры спісу</string>
|
||||||
<string name="suggest_new_sources">Прапаноўваць новыя крыніцы пасля абнаўлення праграмы</string>
|
<string name="suggest_new_sources">Прапаноўваць новыя крыніцы пасля абнаўлення праграмы</string>
|
||||||
<string name="enhanced_colors_summary">Памяншае паласы, але можа паўплываць на прадукцыйнасць</string>
|
<string name="enhanced_colors_summary">Памяншае паласы, але можа паўплываць на прадукцыйнасць</string>
|
||||||
<string name="by_relevance">Актуальнасць</string>
|
<string name="by_relevance">Актуальнасць</string>
|
||||||
<string name="enhanced_colors">32-бітны каляровы рэжым</string>
|
<string name="enhanced_colors">32-бітны каляровы рэжым</string>
|
||||||
<string name="suggest_new_sources_summary">Запыт на ўключэнне зноў дададзеных крыніц пасля абнаўлення праграмы</string>
|
<string name="suggest_new_sources_summary">Прапаноўваць крыніцы мангі, дададзеныя ў апошнім абнаўленні праграмы</string>
|
||||||
|
<string name="online_variant">Анлайн варыянт</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -493,4 +493,5 @@
|
|||||||
<string name="list_options">Lista de opciones</string>
|
<string name="list_options">Lista de opciones</string>
|
||||||
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
|
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
|
||||||
<string name="by_relevance">Relevancia</string>
|
<string name="by_relevance">Relevancia</string>
|
||||||
|
<string name="online_variant">Variante en línea</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
|
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_900</item>
|
||||||
|
<item name="android:windowSplashScreenAnimationDuration">800</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- From ThemeOverlay.Material3.DynamicColors.Dark -->
|
<!-- From ThemeOverlay.Material3.DynamicColors.Dark -->
|
||||||
<style name="Theme.Kotatsu.Monet">
|
<style name="Theme.Kotatsu.Monet">
|
||||||
<item name="isMaterial3DynamicColorApplied">true</item>
|
<item name="isMaterial3DynamicColorApplied">true</item>
|
||||||
|
|||||||
@@ -493,4 +493,5 @@
|
|||||||
<string name="by_relevance">Актуальность</string>
|
<string name="by_relevance">Актуальность</string>
|
||||||
<string name="enhanced_colors">32-битный цветовой режим</string>
|
<string name="enhanced_colors">32-битный цветовой режим</string>
|
||||||
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
|
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
|
||||||
|
<string name="online_variant">Онлайн вариант</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -487,10 +487,11 @@
|
|||||||
<string name="keep_screen_on_summary">Не вимикати екран під час читання манги</string>
|
<string name="keep_screen_on_summary">Не вимикати екран під час читання манги</string>
|
||||||
<string name="state_abandoned">Кинута</string>
|
<string name="state_abandoned">Кинута</string>
|
||||||
<string name="categories">Категорії</string>
|
<string name="categories">Категорії</string>
|
||||||
<string name="list_options">Список опцій</string>
|
<string name="list_options">Параметри списку</string>
|
||||||
<string name="suggest_new_sources">Пропонуйте нові джерела після оновлення застосунка</string>
|
<string name="suggest_new_sources">Пропонуйте нові джерела після оновлення застосунка</string>
|
||||||
<string name="enhanced_colors_summary">Зменшує смуги, але може вплинути на продуктивність</string>
|
<string name="enhanced_colors_summary">Зменшує смуги, але може вплинути на продуктивність</string>
|
||||||
<string name="by_relevance">Актуальність</string>
|
<string name="by_relevance">Актуальність</string>
|
||||||
<string name="enhanced_colors">32-бітний колірний режим</string>
|
<string name="enhanced_colors">32-бітний колірний режим</string>
|
||||||
<string name="suggest_new_sources_summary">Запропонувати ввімкнути щойно додані джерела після оновлення застосунка</string>
|
<string name="suggest_new_sources_summary">Пропонуйте джерела манги, додані в останньому оновленні застосунка</string>
|
||||||
|
<string name="online_variant">Онлайн варіант</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
|
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
|
||||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_50</item>
|
||||||
<item name="android:windowSplashScreenAnimationDuration">800</item>
|
<item name="android:windowSplashScreenAnimationDuration">800</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -499,4 +499,5 @@
|
|||||||
<string name="list_options">List options</string>
|
<string name="list_options">List options</string>
|
||||||
<string name="by_relevance">Relevance</string>
|
<string name="by_relevance">Relevance</string>
|
||||||
<string name="categories">Categories</string>
|
<string name="categories">Categories</string>
|
||||||
|
<string name="online_variant">Online variant</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user