Option to override manga title and cover

This commit is contained in:
Koitharu
2025-04-20 17:20:42 +03:00
parent d542fa6bb6
commit bd4fecc3b6
31 changed files with 620 additions and 76 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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?,
)

View File

@@ -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> {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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>()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 ->

View 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>

View 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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>