Option to override manga title and cover
This commit is contained in:
@@ -208,6 +208,9 @@
|
||||
android:launchMode="singleTop" />
|
||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
||||
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.override.OverrideConfigActivity"
|
||||
android:label="@string/edit" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -8,3 +8,4 @@ const val TABLE_HISTORY = "history"
|
||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||
const val TABLE_SOURCES = "sources"
|
||||
const val TABLE_CHAPTERS = "chapters"
|
||||
const val TABLE_PREFERENCES = "preferences"
|
||||
|
||||
@@ -15,6 +15,9 @@ abstract class PreferencesDao {
|
||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
||||
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
|
||||
|
||||
@Query("SELECT * FROM preferences WHERE title_override IS NOT NULL OR cover_override IS NOT NULL OR content_rating_override IS NOT NULL")
|
||||
abstract suspend fun getOverrides(): List<MangaPrefsEntity>
|
||||
|
||||
@Query("UPDATE preferences SET cf_brightness = 0, cf_contrast = 0, cf_invert = 0, cf_grayscale = 0")
|
||||
abstract suspend fun resetColorFilters()
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_PREFERENCES
|
||||
|
||||
@Entity(
|
||||
tableName = "preferences",
|
||||
tableName = TABLE_PREFERENCES,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.strikeThrough
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
@@ -20,6 +21,7 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.util.findById
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -192,3 +194,14 @@ fun MangaChapter.getLocalizedTitle(resources: Resources, index: Int = -1): Strin
|
||||
else -> resources.getString(R.string.unnamed_chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.withOverride(override: MangaOverride?) = if (override != null) {
|
||||
copy(
|
||||
title = override.title.ifNullOrEmpty { title },
|
||||
coverUrl = override.coverUrl.ifNullOrEmpty { coverUrl },
|
||||
largeCoverUrl = override.coverUrl.ifNullOrEmpty { largeCoverUrl },
|
||||
contentRating = override.contentRating ?: contentRating,
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateActivity
|
||||
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import org.koitharu.kotatsu.settings.override.OverrideConfigActivity
|
||||
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
@@ -249,6 +250,12 @@ class AppRouter private constructor(
|
||||
startActivity(mangaUpdatesIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openMangaOverrideConfig(manga: Manga) {
|
||||
val intent = Intent(contextOrNull() ?: return, OverrideConfigActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga, withDescription = false))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun openSettings() = startActivity(SettingsActivity::class.java)
|
||||
|
||||
fun openReaderSettings() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import androidx.collection.LongObjectMap
|
||||
import androidx.collection.MutableLongObjectMap
|
||||
import androidx.core.net.toUri
|
||||
import androidx.room.withTransaction
|
||||
import dagger.Reusable
|
||||
@@ -7,6 +9,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_PREFERENCES
|
||||
import org.koitharu.kotatsu.core.db.entity.ContentRating
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
@@ -17,10 +21,12 @@ import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.MangaIntent
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
@@ -67,6 +73,33 @@ class MangaDataRepository @Inject constructor(
|
||||
return db.getPreferencesDao().find(mangaId)?.getColorFilterOrNull()
|
||||
}
|
||||
|
||||
suspend fun getOverride(mangaId: Long): MangaOverride? {
|
||||
return db.getPreferencesDao().find(mangaId)?.getOverrideOrNull()
|
||||
}
|
||||
|
||||
suspend fun getOverrides(): LongObjectMap<MangaOverride> {
|
||||
val entities = db.getPreferencesDao().getOverrides()
|
||||
val map = MutableLongObjectMap<MangaOverride>(entities.size)
|
||||
for (entity in entities) {
|
||||
map[entity.mangaId] = entity.getOverrideOrNull() ?: continue
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
suspend fun setOverride(mangaId: Long, override: MangaOverride?) {
|
||||
db.withTransaction {
|
||||
val dao = db.getPreferencesDao()
|
||||
val entity = dao.find(mangaId) ?: newEntity(mangaId)
|
||||
dao.upsert(
|
||||
entity.copy(
|
||||
titleOverride = override?.title?.nullIfEmpty(),
|
||||
coverUrlOverride = override?.coverUrl?.nullIfEmpty(),
|
||||
contentRatingOverride = override?.contentRating?.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun observeColorFilter(mangaId: Long): Flow<ReaderColorFilter?> {
|
||||
return db.getPreferencesDao().observe(mangaId)
|
||||
.map { it?.getColorFilterOrNull() }
|
||||
@@ -146,6 +179,11 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun observeOverridesTrigger(emitInitialState: Boolean) = db.invalidationTracker.createFlow(
|
||||
tables = arrayOf(TABLE_PREFERENCES),
|
||||
emitInitialState = emitInitialState,
|
||||
)
|
||||
|
||||
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
||||
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) {
|
||||
ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale)
|
||||
@@ -154,6 +192,18 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaPrefsEntity.getOverrideOrNull(): MangaOverride? {
|
||||
return if (titleOverride.isNullOrEmpty() && coverUrlOverride.isNullOrEmpty() && contentRatingOverride.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
MangaOverride(
|
||||
coverUrl = coverUrlOverride?.nullIfEmpty(),
|
||||
title = titleOverride?.nullIfEmpty(),
|
||||
contentRating = ContentRating(contentRatingOverride),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
|
||||
mangaId = mangaId,
|
||||
mode = -1,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
|
||||
data class MangaOverride(
|
||||
val coverUrl: String?,
|
||||
val title: String?,
|
||||
val contentRating: ContentRating?,
|
||||
)
|
||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.details.data
|
||||
|
||||
import org.koitharu.kotatsu.core.model.getLocale
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.model.withOverride
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
@@ -13,6 +15,7 @@ import java.util.Locale
|
||||
data class MangaDetails(
|
||||
private val manga: Manga,
|
||||
private val localManga: LocalManga?,
|
||||
private val override: MangaOverride?,
|
||||
val description: CharSequence?,
|
||||
val isLoaded: Boolean,
|
||||
) {
|
||||
@@ -34,12 +37,13 @@ data class MangaDetails(
|
||||
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
|
||||
|
||||
val coverUrl: String?
|
||||
get() = manga.largeCoverUrl
|
||||
get() = override?.coverUrl
|
||||
.ifNullOrEmpty { manga.largeCoverUrl }
|
||||
.ifNullOrEmpty { manga.coverUrl }
|
||||
.ifNullOrEmpty { localManga?.manga?.coverUrl }
|
||||
?.nullIfEmpty()
|
||||
|
||||
fun toManga() = manga
|
||||
fun toManga() = manga.withOverride(override)
|
||||
|
||||
fun getLocale(): Locale? {
|
||||
findAppropriateLocale(chapters.keys.singleOrNull())?.let {
|
||||
@@ -48,13 +52,11 @@ data class MangaDetails(
|
||||
return manga.source.getLocale()
|
||||
}
|
||||
|
||||
fun filterChapters(branch: String?) = MangaDetails(
|
||||
fun filterChapters(branch: String?) = copy(
|
||||
manga = manga.filterChapters(branch),
|
||||
localManga = localManga?.run {
|
||||
copy(manga = manga.filterChapters(branch))
|
||||
},
|
||||
description = description,
|
||||
isLoaded = isLoaded,
|
||||
)
|
||||
|
||||
private fun mergeChapters(): List<MangaChapter> {
|
||||
|
||||
@@ -52,7 +52,16 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
m
|
||||
}
|
||||
}
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
val override = mangaDataRepository.getOverride(manga.id)
|
||||
send(
|
||||
MangaDetails(
|
||||
manga = manga,
|
||||
localManga = null,
|
||||
override = override,
|
||||
description = null,
|
||||
isLoaded = false,
|
||||
),
|
||||
)
|
||||
val local = if (!manga.isLocal) {
|
||||
async {
|
||||
localMangaRepository.findSavedManga(manga)
|
||||
@@ -66,28 +75,31 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
launch { updateTracker(details) }
|
||||
send(
|
||||
MangaDetails(
|
||||
details,
|
||||
local?.peek(),
|
||||
details.description?.parseAsHtml(withImages = false)?.trim(),
|
||||
false,
|
||||
manga = details,
|
||||
localManga = local?.peek(),
|
||||
override = override,
|
||||
description = details.description?.parseAsHtml(withImages = false)?.trim(),
|
||||
isLoaded = false,
|
||||
),
|
||||
)
|
||||
send(
|
||||
MangaDetails(
|
||||
details,
|
||||
local?.await(),
|
||||
details.description?.parseAsHtml(withImages = true)?.trim(),
|
||||
true,
|
||||
manga = details,
|
||||
localManga = local?.await(),
|
||||
override = override,
|
||||
description = details.description?.parseAsHtml(withImages = true)?.trim(),
|
||||
isLoaded = true,
|
||||
),
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
local?.await()?.manga?.also { localManga ->
|
||||
send(
|
||||
MangaDetails(
|
||||
localManga,
|
||||
null,
|
||||
localManga.description?.parseAsHtml(withImages = false)?.trim(),
|
||||
true,
|
||||
manga = localManga,
|
||||
localManga = null,
|
||||
override = override,
|
||||
description = localManga.description?.parseAsHtml(withImages = false)?.trim(),
|
||||
isLoaded = true,
|
||||
),
|
||||
)
|
||||
} ?: close(e)
|
||||
|
||||
@@ -87,7 +87,7 @@ class DetailsViewModel @Inject constructor(
|
||||
val mangaId = intent.mangaId
|
||||
|
||||
init {
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, null, false) }
|
||||
}
|
||||
|
||||
val history = historyRepository.observeOne(mangaId)
|
||||
|
||||
@@ -14,12 +14,12 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
@@ -34,8 +34,8 @@ class RelatedListViewModel @Inject constructor(
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
settings: AppSettings,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler) {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository) {
|
||||
|
||||
private val seed = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
private val repository = mangaRepositoryFactory.create(seed.source)
|
||||
|
||||
@@ -93,7 +93,7 @@ class FavouritesCategoryEditActivity :
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
viewBinding.buttonDone.isEnabled = !s.isNullOrBlank()
|
||||
viewBinding.buttonDone.isEnabled = !s.isNullOrBlank() && !viewModel.isLoading.value
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
@@ -119,6 +119,7 @@ class FavouritesCategoryEditActivity :
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
viewBinding.buttonDone.isEnabled = !isLoading && !viewBinding.editName.text.isNullOrBlank()
|
||||
viewBinding.editSort.isEnabled = !isLoading
|
||||
viewBinding.editName.isEnabled = !isLoading
|
||||
viewBinding.switchTracker.isEnabled = !isLoading
|
||||
|
||||
@@ -17,13 +17,13 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.flattenLatest
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||
@@ -51,8 +51,8 @@ class FavouritesListViewModel @Inject constructor(
|
||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||
quickFilterFactory: FavoritesListQuickFilter.Factory,
|
||||
settings: AppSettings,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener {
|
||||
|
||||
val categoryId: Long = savedStateHandle[AppRouter.KEY_ID] ?: NO_ID
|
||||
private val quickFilter = quickFilterFactory.create(categoryId)
|
||||
@@ -92,7 +92,8 @@ class FavouritesListViewModel @Inject constructor(
|
||||
|
||||
override fun onRetry() = Unit
|
||||
|
||||
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) = quickFilter.setFilterOption(option, isApplied)
|
||||
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) =
|
||||
quickFilter.setFilterOption(option, isApplied)
|
||||
|
||||
override fun toggleFilterOption(option: ListFilterOption) = quickFilter.toggleFilterOption(option)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
@@ -22,7 +23,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.flattenLatest
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryListQuickFilter
|
||||
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
||||
@@ -53,8 +53,8 @@ class HistoryListViewModel @Inject constructor(
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||
private val quickFilter: HistoryListQuickFilter,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
|
||||
private val sortOrder: StateFlow<ListSortOrder> = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.IO,
|
||||
|
||||
@@ -6,10 +6,13 @@ import androidx.annotation.ColorRes
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.MutableScatterSet
|
||||
import androidx.collection.ScatterSet
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
@@ -20,11 +23,11 @@ import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Reusable
|
||||
class MangaListMapper @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val settings: AppSettings,
|
||||
@@ -32,6 +35,7 @@ class MangaListMapper @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val localMangaIndex: LocalMangaIndex,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
) {
|
||||
|
||||
private val dict by lazy { readTagsDict(context) }
|
||||
@@ -40,9 +44,13 @@ class MangaListMapper @Inject constructor(
|
||||
manga: Collection<Manga>,
|
||||
mode: ListMode,
|
||||
@Flags flags: Int = DEFAULTS,
|
||||
): List<MangaListModel> {
|
||||
val options = getOptions(flags)
|
||||
return manga.map { toListModelImpl(it, mode, options) }
|
||||
): List<MangaListModel> = ArrayList<MangaListModel>(manga.size).apply {
|
||||
toListModelList(
|
||||
destination = this,
|
||||
manga = manga,
|
||||
mode = mode,
|
||||
flags = flags,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun toListModelList(
|
||||
@@ -52,8 +60,9 @@ class MangaListMapper @Inject constructor(
|
||||
@Flags flags: Int = DEFAULTS,
|
||||
) {
|
||||
val options = getOptions(flags)
|
||||
val overrides = dataRepository.getOverrides()
|
||||
manga.mapTo(destination) {
|
||||
toListModelImpl(it, mode, options)
|
||||
toListModelImpl(it, mode, options, overrides[it.id])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +70,12 @@ class MangaListMapper @Inject constructor(
|
||||
manga: Manga,
|
||||
mode: ListMode,
|
||||
@Flags flags: Int = DEFAULTS,
|
||||
): MangaListModel = toListModelImpl(manga, mode, getOptions(flags))
|
||||
): MangaListModel = toListModelImpl(
|
||||
manga = manga,
|
||||
mode = mode,
|
||||
options = getOptions(flags),
|
||||
override = dataRepository.getOverride(manga.id),
|
||||
)
|
||||
|
||||
fun mapTags(tags: Collection<MangaTag>) = tags.map {
|
||||
ChipsView.ChipModel(
|
||||
@@ -71,20 +85,28 @@ class MangaListMapper @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun toCompactListModel(manga: Manga, @Options options: Int) = MangaCompactListModel(
|
||||
private suspend fun toCompactListModel(
|
||||
manga: Manga,
|
||||
@Options options: Int,
|
||||
override: MangaOverride?,
|
||||
) = MangaCompactListModel(
|
||||
id = manga.id,
|
||||
title = manga.title,
|
||||
title = override?.title.ifNullOrEmpty { manga.title },
|
||||
subtitle = manga.tags.joinToString(", ") { it.title },
|
||||
coverUrl = manga.coverUrl,
|
||||
coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga = manga,
|
||||
counter = getCounter(manga.id, options),
|
||||
)
|
||||
|
||||
private suspend fun toDetailedListModel(manga: Manga, @Options options: Int) = MangaDetailedListModel(
|
||||
private suspend fun toDetailedListModel(
|
||||
manga: Manga,
|
||||
@Options options: Int,
|
||||
override: MangaOverride?,
|
||||
) = MangaDetailedListModel(
|
||||
id = manga.id,
|
||||
title = manga.title,
|
||||
subtitle = manga.altTitle,
|
||||
coverUrl = manga.coverUrl,
|
||||
title = override?.title.ifNullOrEmpty { manga.title },
|
||||
subtitle = manga.altTitles.firstOrNull(),
|
||||
coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga = manga,
|
||||
counter = getCounter(manga.id, options),
|
||||
progress = getProgress(manga.id, options),
|
||||
@@ -93,10 +115,14 @@ class MangaListMapper @Inject constructor(
|
||||
tags = mapTags(manga.tags),
|
||||
)
|
||||
|
||||
private suspend fun toGridModel(manga: Manga, @Options options: Int) = MangaGridModel(
|
||||
private suspend fun toGridModel(
|
||||
manga: Manga,
|
||||
@Options options: Int,
|
||||
override: MangaOverride?
|
||||
) = MangaGridModel(
|
||||
id = manga.id,
|
||||
title = manga.title,
|
||||
coverUrl = manga.coverUrl,
|
||||
title = override?.title.ifNullOrEmpty { manga.title },
|
||||
coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga = manga,
|
||||
counter = getCounter(manga.id, options),
|
||||
progress = getProgress(manga.id, options),
|
||||
@@ -107,11 +133,12 @@ class MangaListMapper @Inject constructor(
|
||||
private suspend fun toListModelImpl(
|
||||
manga: Manga,
|
||||
mode: ListMode,
|
||||
@Options options: Int
|
||||
@Options options: Int,
|
||||
override: MangaOverride?,
|
||||
): MangaListModel = when (mode) {
|
||||
ListMode.LIST -> toCompactListModel(manga, options)
|
||||
ListMode.DETAILED_LIST -> toDetailedListModel(manga, options)
|
||||
ListMode.GRID -> toGridModel(manga, options)
|
||||
ListMode.LIST -> toCompactListModel(manga, options, override)
|
||||
ListMode.DETAILED_LIST -> toDetailedListModel(manga, options, override)
|
||||
ListMode.GRID -> toGridModel(manga, options, override)
|
||||
}
|
||||
|
||||
private suspend fun getCounter(mangaId: Long, @Options options: Int): Int {
|
||||
|
||||
@@ -275,8 +275,10 @@ abstract class MangaListFragment :
|
||||
@CallSuper
|
||||
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode?, menu: Menu): Boolean {
|
||||
val hasNoLocal = selectedItems.none { it.isLocal }
|
||||
val isSingleSelection = controller.count == 1
|
||||
menu.findItem(R.id.action_save)?.isVisible = hasNoLocal
|
||||
menu.findItem(R.id.action_fix)?.isVisible = hasNoLocal
|
||||
menu.findItem(R.id.action_edit_override)?.isVisible = isSingleSelection
|
||||
return super.onPrepareActionMode(controller, mode, menu)
|
||||
}
|
||||
|
||||
@@ -316,6 +318,12 @@ abstract class MangaListFragment :
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_edit_override -> {
|
||||
router.openMangaOverrideConfig(selectedItems.singleOrNull() ?: return false)
|
||||
mode?.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_fix -> {
|
||||
val itemsSnapshot = selectedItemsIds
|
||||
buildAlertDialog(context ?: return false, isCentered = true) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
@@ -17,14 +18,13 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
abstract class MangaListViewModel(
|
||||
private val settings: AppSettings,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val content: StateFlow<List<ListModel>>
|
||||
@@ -62,13 +62,14 @@ abstract class MangaListViewModel(
|
||||
|
||||
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
|
||||
listMode,
|
||||
mangaDataRepository.observeOverridesTrigger(emitInitialState = true),
|
||||
settings.observe().filter { key ->
|
||||
key == AppSettings.KEY_PROGRESS_INDICATORS
|
||||
|| key == AppSettings.KEY_TRACKER_ENABLED
|
||||
|| key == AppSettings.KEY_QUICK_FILTER
|
||||
|| key == AppSettings.KEY_MANGA_LIST_BADGES
|
||||
}.onStart { emit("") },
|
||||
) { mode, _ ->
|
||||
) { mode, _, _ ->
|
||||
mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -13,7 +14,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
@@ -36,22 +36,22 @@ class LocalListViewModel @Inject constructor(
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
filterCoordinator: FilterCoordinator,
|
||||
private val settings: AppSettings,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
mangaListMapper: MangaListMapper,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
exploreRepository: ExploreRepository,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val localStorageManager: LocalStorageManager,
|
||||
sourcesRepository: MangaSourcesRepository,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : RemoteListViewModel(
|
||||
savedStateHandle,
|
||||
mangaRepositoryFactory,
|
||||
filterCoordinator,
|
||||
settings,
|
||||
mangaListMapper,
|
||||
downloadScheduler,
|
||||
exploreRepository,
|
||||
sourcesRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
filterCoordinator = filterCoordinator,
|
||||
settings = settings,
|
||||
mangaListMapper = mangaListMapper,
|
||||
exploreRepository = exploreRepository,
|
||||
sourcesRepository = sourcesRepository,
|
||||
mangaDataRepository = mangaDataRepository,
|
||||
), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Unit>()
|
||||
|
||||
@@ -106,7 +106,7 @@ class ReaderViewModel @Inject constructor(
|
||||
init {
|
||||
selectedBranch.value = savedStateHandle.get<String>(ReaderIntent.EXTRA_BRANCH)
|
||||
readingState.value = savedStateHandle[ReaderIntent.EXTRA_STATE]
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, false) }
|
||||
mangaDetails.value = intent.manga?.let { MangaDetails(it, null, null, null, false) }
|
||||
}
|
||||
|
||||
val readerMode = MutableStateFlow<ReaderMode?>(null)
|
||||
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.distinctById
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -27,7 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
@@ -53,10 +53,10 @@ open class RemoteListViewModel @Inject constructor(
|
||||
final override val filterCoordinator: FilterCoordinator,
|
||||
settings: AppSettings,
|
||||
protected val mangaListMapper: MangaListMapper,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val exploreRepository: ExploreRepository,
|
||||
sourcesRepository: MangaSourcesRepository,
|
||||
) : MangaListViewModel(settings, downloadScheduler), FilterCoordinator.Owner {
|
||||
mangaDataRepository: MangaDataRepository
|
||||
) : MangaListViewModel(settings, mangaDataRepository), FilterCoordinator.Owner {
|
||||
|
||||
val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE])
|
||||
val isRandomLoading = MutableStateFlow(false)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.koitharu.kotatsu.settings.override
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.lifecycle
|
||||
import coil3.request.target
|
||||
import coil3.size.Scale
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeAll
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.databinding.ActivityOverrideEditBinding
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
import javax.inject.Inject
|
||||
import androidx.appcompat.R as appcompatR
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OverrideConfigActivity : BaseActivity<ActivityOverrideEditBinding>(), View.OnClickListener {
|
||||
|
||||
private val viewModel: OverrideConfigViewModel by viewModels()
|
||||
|
||||
private val pickCoverFileLauncher = registerForActivityResult(
|
||||
PickVisualMedia(),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
viewModel.updateCover(uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityOverrideEditBinding.inflate(layoutInflater))
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
viewBinding.buttonDone.setOnClickListener(this)
|
||||
viewBinding.buttonPickFile.setOnClickListener(this)
|
||||
viewBinding.buttonPickPage.setOnClickListener(this)
|
||||
viewBinding.buttonResetCover.setOnClickListener(this)
|
||||
viewBinding.layoutName.setEndIconOnClickListener(this)
|
||||
viewModel.data.filterNotNull().observe(this, ::onDataChanged)
|
||||
viewModel.onSaved.observeEvent(this) { finishAfterTransition() }
|
||||
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
|
||||
viewModel.onError.observeEvent(this, ::onError)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||
val barsInsets = insets.getInsets(typeMask)
|
||||
viewBinding.root.setPadding(
|
||||
barsInsets.left,
|
||||
barsInsets.top,
|
||||
barsInsets.right,
|
||||
barsInsets.bottom,
|
||||
)
|
||||
return insets.consumeAll(typeMask)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_done -> viewModel.save(
|
||||
title = viewBinding.editName.text?.toString()?.trim(),
|
||||
)
|
||||
|
||||
materialR.id.text_input_end_icon -> viewBinding.editName.text?.clear()
|
||||
|
||||
R.id.button_reset_cover -> viewModel.updateCover(null)
|
||||
R.id.button_pick_file -> {
|
||||
val request = PickVisualMediaRequest.Builder()
|
||||
.setMediaType(PickVisualMedia.ImageOnly)
|
||||
.setAccentColor(getThemeColor(appcompatR.attr.colorAccent).toLong())
|
||||
.build()
|
||||
if (!pickCoverFileLauncher.tryLaunch(request)) {
|
||||
Snackbar.make(
|
||||
viewBinding.imageViewCover,
|
||||
R.string.operation_not_supported,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDataChanged(data: Pair<Manga, MangaOverride>) {
|
||||
val (manga, override) = data
|
||||
ImageRequest.Builder(this)
|
||||
.target(viewBinding.imageViewCover)
|
||||
.size(CoverSizeResolver(viewBinding.imageViewCover))
|
||||
.scale(Scale.FILL)
|
||||
.data(override.coverUrl.ifNullOrEmpty { manga.coverUrl })
|
||||
.mangaSourceExtra(manga.source)
|
||||
.crossfade(this)
|
||||
.lifecycle(this)
|
||||
.enqueueWith(coil)
|
||||
viewBinding.layoutName.placeholderText = manga.title
|
||||
if (viewBinding.editName.tag == null) {
|
||||
viewBinding.editName.setText(override.title)
|
||||
viewBinding.editName.tag = override.title
|
||||
}
|
||||
viewBinding.buttonResetCover.isEnabled = !override.coverUrl.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
viewBinding.textViewError.text = e.getDisplayMessage(resources)
|
||||
viewBinding.textViewError.isVisible = true
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
viewBinding.buttonDone.isEnabled = !isLoading
|
||||
viewBinding.editName.isEnabled = !isLoading
|
||||
if (isLoading) {
|
||||
viewBinding.textViewError.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.koitharu.kotatsu.settings.override
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.model.MangaOverride
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class OverrideConfigViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
|
||||
val data = MutableStateFlow<Pair<Manga, MangaOverride>?>(null)
|
||||
val onSaved = MutableEventFlow<Unit>()
|
||||
|
||||
init {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
data.value = manga to (dataRepository.getOverride(manga.id) ?: emptyOverride())
|
||||
}
|
||||
}
|
||||
|
||||
fun save(title: String?) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val override = checkNotNull(data.value).second.copy(
|
||||
title = title,
|
||||
)
|
||||
dataRepository.setOverride(manga.id, override)
|
||||
onSaved.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCover(coverUri: String?) {
|
||||
val snapshot = data.value ?: return
|
||||
data.value = snapshot.first to snapshot.second.copy(
|
||||
coverUrl = coverUri,
|
||||
)
|
||||
}
|
||||
|
||||
private fun emptyOverride() = MangaOverride(null, null, null)
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
@@ -30,10 +30,10 @@ class SuggestionsViewModel @Inject constructor(
|
||||
repository: SuggestionRepository,
|
||||
settings: AppSettings,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val quickFilter: SuggestionsListQuickFilter,
|
||||
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
|
||||
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_SUGGESTIONS) { suggestionsListMode }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode)
|
||||
|
||||
@@ -11,13 +11,13 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||
@@ -38,8 +38,8 @@ class UpdatesViewModel @Inject constructor(
|
||||
settings: AppSettings,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val quickFilter: UpdatesListQuickFilter,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
) : MangaListViewModel(settings, mangaDataRepository), QuickFilterListener by quickFilter {
|
||||
|
||||
override val content = combine(
|
||||
quickFilter.appliedOptions.flatMapLatest { filterOptions ->
|
||||
|
||||
13
app/src/main/res/drawable/ic_revert.xml
Normal file
13
app/src/main/res/drawable/ic_revert.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13.5,7A6.5,6.5 0 0,1 20,13.5A6.5,6.5 0 0,1 13.5,20H10V18H13.5C16,18 18,16 18,13.5C18,11 16,9 13.5,9H7.83L10.91,12.09L9.5,13.5L4,8L9.5,2.5L10.92,3.91L7.83,7H13.5M6,18H8V20H6V18Z" />
|
||||
|
||||
</vector>
|
||||
177
app/src/main/res/layout/activity_override_edit.xml
Normal file
177
app/src/main/res/layout/activity_override_edit.xml
Normal file
@@ -0,0 +1,177 @@
|
||||
<?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="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_done"
|
||||
style="@style/Widget.Material3.Button.UnelevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
|
||||
android:text="@string/save" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="ifContentScrolls">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/screen_padding">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?colorSecondaryContainer"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,13:18"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.3"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:background="@tools:sample/backgrounds/scenic[5]"
|
||||
tools:ignore="ContentDescription,UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_cover_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||
android:paddingHorizontal="@dimen/margin_small"
|
||||
android:text="@string/change_cover"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="@id/imageView_cover" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_pick_file"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_marginTop="4dp"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/pick_custom_file"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
app:drawableStartCompat="@drawable/ic_folder_file"
|
||||
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_cover_title" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_pick_page"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/pick_manga_page"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_grid"
|
||||
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_pick_file" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_reset_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/use_default_cover"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
app:drawableStartCompat="@drawable/ic_revert"
|
||||
app:layout_constraintEnd_toEndOf="@id/textView_cover_title"
|
||||
app:layout_constraintStart_toStartOf="@id/textView_cover_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_pick_page" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_cover"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="imageView_cover,button_reset_cover" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
app:endIconContentDescription="@string/reset"
|
||||
app:endIconDrawable="@drawable/ic_revert"
|
||||
app:endIconMode="custom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_cover">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/name"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textCapSentences"
|
||||
android:maxLength="120"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_tip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:text="@string/manga_override_hint"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/screen_padding"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:textColor="?colorError"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_tip"
|
||||
tools:text="@tools:sample/lorem[4]"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -33,6 +33,12 @@
|
||||
android:title="@string/categories"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_override"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_mark_current"
|
||||
android:icon="@drawable/ic_eye_check"
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
android:title="@string/fix"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_override"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_mark_current"
|
||||
android:icon="@drawable/ic_eye_check"
|
||||
|
||||
@@ -15,9 +15,15 @@
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_override"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:icon="?actionModeSelectAllDrawable"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -826,4 +826,9 @@
|
||||
<string name="tags_warnings">Highlight dangerous genres</string>
|
||||
<string name="tags_warnings_summary">Highlight genres that may be inappropriate for most users</string>
|
||||
<string name="error_non_file_uri">The selected path cannot be used because it does not denote a file or directory</string>
|
||||
<string name="manga_override_hint">These changes will affect how manga is displayed in the app</string>
|
||||
<string name="use_default_cover">Use default cover</string>
|
||||
<string name="pick_manga_page">Pick manga page</string>
|
||||
<string name="pick_custom_file">Pick custom file</string>
|
||||
<string name="change_cover">Change cover</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user