feat: Hidden categories (#348)
* support hide categories (merged from Aniyomi) * styling CategoryScreen: always showing all categories & shading the hidden one * Hide categories directly on Library tab and load it as a flow * fix: hidden categories got reset after delete/reorder * remove unnecessary code
This commit is contained in:
parent
4a2f8a7895
commit
6c9dfb82f4
21 changed files with 201 additions and 13 deletions
|
|
@ -46,6 +46,7 @@ import tachiyomi.data.updates.UpdatesRepositoryImpl
|
|||
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
||||
import tachiyomi.domain.category.interactor.DeleteCategory
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.HideCategory
|
||||
import tachiyomi.domain.category.interactor.RenameCategory
|
||||
import tachiyomi.domain.category.interactor.ReorderCategory
|
||||
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
||||
|
|
@ -110,6 +111,9 @@ class DomainModule : InjektModule {
|
|||
addFactory { ReorderCategory(get()) }
|
||||
addFactory { UpdateCategory(get()) }
|
||||
addFactory { DeleteCategory(get()) }
|
||||
// KMK -->
|
||||
addFactory { HideCategory(get()) }
|
||||
// KMK <--
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
addFactory { GetDuplicateLibraryManga(get()) }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ fun CategoryScreen(
|
|||
onClickDelete: (Category) -> Unit,
|
||||
onClickMoveUp: (Category) -> Unit,
|
||||
onClickMoveDown: (Category) -> Unit,
|
||||
// KMK -->
|
||||
onClickHide: (Category) -> Unit,
|
||||
// KMK <--
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
|
@ -83,6 +86,9 @@ fun CategoryScreen(
|
|||
onClickDelete = onClickDelete,
|
||||
onMoveUp = onClickMoveUp,
|
||||
onMoveDown = onClickMoveDown,
|
||||
// KMK -->
|
||||
onClickHide = onClickHide,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +102,9 @@ private fun CategoryContent(
|
|||
onClickDelete: (Category) -> Unit,
|
||||
onMoveUp: (Category) -> Unit,
|
||||
onMoveDown: (Category) -> Unit,
|
||||
// KMK -->
|
||||
onClickHide: (Category) -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
|
|
@ -115,6 +124,9 @@ private fun CategoryContent(
|
|||
onMoveDown = onMoveDown,
|
||||
onRename = { onClickRename(category) },
|
||||
onDelete = { onClickDelete(category) },
|
||||
// KMK -->
|
||||
onHide = { onClickHide(category) },
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,16 +11,21 @@ import androidx.compose.material.icons.outlined.ArrowDropDown
|
|||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
|
|
@ -33,6 +38,9 @@ fun CategoryListItem(
|
|||
onMoveDown: (Category) -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
// KMK -->
|
||||
onHide: () -> Unit,
|
||||
// KMK <--
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
|
|
@ -49,9 +57,19 @@ fun CategoryListItem(
|
|||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.Label,
|
||||
contentDescription = null,
|
||||
// KMK -->
|
||||
tint = LocalContentColor.current.let { if (category.hidden) it.copy(alpha = 0.6f) else it },
|
||||
// KMK <--
|
||||
)
|
||||
Text(
|
||||
text = category.name,
|
||||
// KMK -->
|
||||
color = LocalContentColor.current.let { if (category.hidden) it.copy(alpha = 0.6f) else it },
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { category.hidden },
|
||||
// KMK <--
|
||||
modifier = Modifier
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
)
|
||||
|
|
@ -76,6 +94,21 @@ fun CategoryListItem(
|
|||
contentDescription = stringResource(MR.strings.action_rename_category),
|
||||
)
|
||||
}
|
||||
// KMK -->
|
||||
IconButton(
|
||||
onClick = onHide,
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = if (category.hidden) {
|
||||
Icons.Outlined.Visibility
|
||||
} else {
|
||||
Icons.Outlined.VisibilityOff
|
||||
},
|
||||
contentDescription = stringResource(KMR.strings.action_hide),
|
||||
)
|
||||
},
|
||||
)
|
||||
// KMK <--
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,12 @@ private fun ColumnScope.DisplayPage(
|
|||
label = stringResource(MR.strings.action_display_show_tabs),
|
||||
pref = screenModel.libraryPreferences.categoryTabs(),
|
||||
)
|
||||
// KMK -->
|
||||
CheckboxItem(
|
||||
label = stringResource(KMR.strings.action_hide_hidden_categories),
|
||||
pref = screenModel.libraryPreferences.hideHiddenCategories(),
|
||||
)
|
||||
// KMK <--
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_show_number_of_items),
|
||||
pref = screenModel.libraryPreferences.categoryNumberOfItems(),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class BackupCategory(
|
|||
@ProtoNumber(2) var order: Long = 0,
|
||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||
@ProtoNumber(100) var flags: Long = 0,
|
||||
// KMK -->
|
||||
@ProtoNumber(900) var hidden: Boolean = false,
|
||||
// KMK <--
|
||||
// SY specific values
|
||||
/*@ProtoNumber(600) var mangaOrder: List<Long> = emptyList(),*/
|
||||
) {
|
||||
|
|
@ -18,6 +21,9 @@ class BackupCategory(
|
|||
name = this@BackupCategory.name,
|
||||
flags = this@BackupCategory.flags,
|
||||
order = this@BackupCategory.order,
|
||||
// KMK -->
|
||||
hidden = this@BackupCategory.hidden,
|
||||
// KMK <--
|
||||
/*mangaOrder = this@BackupCategory.mangaOrder*/
|
||||
)
|
||||
}
|
||||
|
|
@ -27,5 +33,8 @@ val backupCategoryMapper = { category: Category ->
|
|||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
// KMK -->
|
||||
hidden = category.hidden,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,12 @@ class CategoriesRestorer(
|
|||
if (dbCategory != null) return@map dbCategory
|
||||
val order = nextOrder++
|
||||
handler.awaitOneExecutable {
|
||||
categoriesQueries.insert(it.name, order, it.flags)
|
||||
categoriesQueries.insert(
|
||||
it.name, order, it.flags,
|
||||
// KMK -->
|
||||
hidden = if (it.hidden) 1L else 0L,
|
||||
// KMK <--
|
||||
)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
.let { id -> it.toCategory(id).copy(order = order) }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class CategoryScreen : Screen() {
|
|||
onClickDelete = { screenModel.showDialog(CategoryDialog.Delete(it)) },
|
||||
onClickMoveUp = screenModel::moveUp,
|
||||
onClickMoveDown = screenModel::moveDown,
|
||||
// KMK -->
|
||||
onClickHide = screenModel::hideCategory,
|
||||
// KMK <--
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
|||
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
||||
import tachiyomi.domain.category.interactor.DeleteCategory
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.HideCategory
|
||||
import tachiyomi.domain.category.interactor.RenameCategory
|
||||
import tachiyomi.domain.category.interactor.ReorderCategory
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
|
@ -27,6 +28,9 @@ class CategoryScreenModel(
|
|||
private val deleteCategory: DeleteCategory = Injekt.get(),
|
||||
private val reorderCategory: ReorderCategory = Injekt.get(),
|
||||
private val renameCategory: RenameCategory = Injekt.get(),
|
||||
// KMK -->
|
||||
private val hideCategory: HideCategory = Injekt.get(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<CategoryScreenState>(CategoryScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<CategoryEvent> = Channel()
|
||||
|
|
@ -56,6 +60,17 @@ class CategoryScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
fun hideCategory(category: Category) {
|
||||
screenModelScope.launch {
|
||||
when (hideCategory.await(category)) {
|
||||
is HideCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
fun deleteCategory(categoryId: Long) {
|
||||
screenModelScope.launch {
|
||||
when (deleteCategory.await(categoryId = categoryId)) {
|
||||
|
|
|
|||
|
|
@ -592,7 +592,13 @@ class LibraryScreenModel(
|
|||
.groupBy { it.libraryManga.category }
|
||||
}
|
||||
|
||||
return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga ->
|
||||
return combine(
|
||||
// KMK -->
|
||||
libraryPreferences.hideHiddenCategories().changes(),
|
||||
// KMK <--
|
||||
getCategories.subscribe(),
|
||||
libraryMangasFlow,
|
||||
) { hideHiddenCategories, categories, libraryManga ->
|
||||
val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) {
|
||||
categories.fastFilterNot { it.isSystemCategory }
|
||||
} else {
|
||||
|
|
@ -604,7 +610,11 @@ class LibraryScreenModel(
|
|||
state.copy(ogCategories = displayCategories)
|
||||
}
|
||||
// SY <--
|
||||
displayCategories.associateWith { libraryManga[it.id].orEmpty() }
|
||||
displayCategories
|
||||
// KMK -->
|
||||
.filterNot { hideHiddenCategories && it.hidden }
|
||||
// KMK <--
|
||||
.associateWith { libraryManga[it.id].orEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -619,6 +629,9 @@ class LibraryScreenModel(
|
|||
preferences.context.stringResource(SYMR.strings.ungrouped),
|
||||
0,
|
||||
0,
|
||||
// KMK -->
|
||||
false,
|
||||
// KMK <--
|
||||
) to
|
||||
values.flatten().distinctBy { it.libraryManga.manga.id },
|
||||
)
|
||||
|
|
@ -1283,6 +1296,9 @@ class LibraryScreenModel(
|
|||
it.int == id
|
||||
}.takeUnless { it == -1 }?.toLong() ?: TrackStatus.OTHER.ordinal.toLong(),
|
||||
flags = 0,
|
||||
// KMK -->
|
||||
hidden = false,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1308,6 +1324,9 @@ class LibraryScreenModel(
|
|||
},
|
||||
order = sources.indexOf(it.key).takeUnless { it == -1 }?.toLong() ?: Long.MAX_VALUE,
|
||||
flags = 0,
|
||||
// KMK -->
|
||||
hidden = false,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1336,6 +1355,9 @@ class LibraryScreenModel(
|
|||
else -> 7
|
||||
},
|
||||
flags = 0,
|
||||
// KMK -->
|
||||
hidden = false,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ class MoveSortingModeSettingsMigration : Migration {
|
|||
flags = it.flags and 0b00111100L.inv(),
|
||||
name = null,
|
||||
order = null,
|
||||
// KMK -->
|
||||
hidden = null,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ class Tester {
|
|||
name = "a",
|
||||
order = 1,
|
||||
flags = 0,
|
||||
// KMK -->
|
||||
hidden = false,
|
||||
// KMK <--
|
||||
),
|
||||
)
|
||||
val favoriteEntries = listOf(
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@ object CategoryMapper {
|
|||
name: String,
|
||||
order: Long,
|
||||
flags: Long,
|
||||
// KMK -->
|
||||
hidden: Long,
|
||||
// KMK <--
|
||||
): Category {
|
||||
return Category(
|
||||
id = id,
|
||||
name = name,
|
||||
order = order,
|
||||
flags = flags,
|
||||
// KMK -->
|
||||
hidden = hidden == 1L,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class CategoryRepositoryImpl(
|
|||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
// KMK -->
|
||||
hidden = if (category.hidden) 1L else 0L,
|
||||
// KMK <--
|
||||
)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}
|
||||
|
|
@ -67,6 +70,9 @@ class CategoryRepositoryImpl(
|
|||
name = update.name,
|
||||
order = update.order,
|
||||
flags = update.flags,
|
||||
// KMK -->
|
||||
hidden = update.hidden?.let { if (it) 1L else 0L },
|
||||
// KMK <--
|
||||
categoryId = update.id,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ CREATE TABLE categories(
|
|||
name TEXT NOT NULL,
|
||||
sort INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
manga_order TEXT AS List<Long> NOT NULL
|
||||
manga_order TEXT AS List<Long> NOT NULL,
|
||||
-- KMK -->
|
||||
hidden INTEGER NOT NULL DEFAULT 0
|
||||
-- KMK <--
|
||||
);
|
||||
|
||||
-- Insert system category
|
||||
INSERT OR IGNORE INTO categories(_id, name, sort, flags, manga_order) VALUES (0, "", -1, 0, "");
|
||||
INSERT OR IGNORE INTO categories(_id, name, sort, flags, manga_order, hidden) VALUES (0, "", -1, 0, "", 0);
|
||||
-- Disallow deletion of default category
|
||||
CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE
|
||||
ON categories
|
||||
|
|
@ -21,7 +24,7 @@ BEGIN SELECT CASE
|
|||
END;
|
||||
|
||||
getCategory:
|
||||
SELECT _id,name,sort,flags
|
||||
SELECT _id,name,sort,flags,hidden
|
||||
FROM categories
|
||||
WHERE _id = :id
|
||||
LIMIT 1;
|
||||
|
|
@ -31,7 +34,10 @@ SELECT
|
|||
_id AS id,
|
||||
name,
|
||||
sort AS `order`,
|
||||
flags
|
||||
flags,
|
||||
-- KMK -->
|
||||
hidden
|
||||
-- KMK <--
|
||||
FROM categories
|
||||
ORDER BY sort;
|
||||
|
||||
|
|
@ -40,15 +46,18 @@ SELECT
|
|||
C._id AS id,
|
||||
C.name,
|
||||
C.sort AS `order`,
|
||||
C.flags
|
||||
C.flags,
|
||||
-- KMK -->
|
||||
C.hidden
|
||||
-- KMK <--
|
||||
FROM categories C
|
||||
JOIN mangas_categories MC
|
||||
ON C._id = MC.category_id
|
||||
WHERE MC.manga_id = :mangaId;
|
||||
|
||||
insert:
|
||||
INSERT INTO categories(name, sort, flags, manga_order)
|
||||
VALUES (:name, :order, :flags, "");
|
||||
INSERT INTO categories(name, sort, flags, manga_order, hidden)
|
||||
VALUES (:name, :order, :flags, "", :hidden);
|
||||
|
||||
delete:
|
||||
DELETE FROM categories
|
||||
|
|
@ -58,7 +67,10 @@ update:
|
|||
UPDATE categories
|
||||
SET name = coalesce(:name, name),
|
||||
sort = coalesce(:order, sort),
|
||||
flags = coalesce(:flags, flags)
|
||||
flags = coalesce(:flags, flags),
|
||||
-- KMK -->
|
||||
hidden = coalesce(:hidden, hidden)
|
||||
-- KMK <--
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
updateAllFlags:
|
||||
|
|
@ -66,4 +78,4 @@ UPDATE categories SET
|
|||
flags = coalesce(?, flags);
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
SELECT last_insert_rowid();
|
||||
|
|
|
|||
1
data/src/main/sqldelight/tachiyomi/migrations/34.sqm
Normal file
1
data/src/main/sqldelight/tachiyomi/migrations/34.sqm
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0 NOT NULL;
|
||||
|
|
@ -26,6 +26,9 @@ class CreateCategoryWithName(
|
|||
name = name,
|
||||
order = nextOrder,
|
||||
flags = initialFlags,
|
||||
// KMK -->
|
||||
hidden = false,
|
||||
// KMK <--
|
||||
)
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package tachiyomi.domain.category.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class HideCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(category: Category) = withNonCancellableContext {
|
||||
val update = CategoryUpdate(
|
||||
id = category.id,
|
||||
hidden = !category.hidden,
|
||||
)
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(update)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,9 @@ data class Category(
|
|||
val name: String,
|
||||
val order: Long,
|
||||
val flags: Long,
|
||||
// KMK -->
|
||||
val hidden: Boolean,
|
||||
// KMK <--
|
||||
) : Serializable {
|
||||
|
||||
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@ data class CategoryUpdate(
|
|||
val name: String? = null,
|
||||
val order: Long? = null,
|
||||
val flags: Long? = null,
|
||||
// KMK -->
|
||||
val hidden: Boolean? = null,
|
||||
// KMK <--
|
||||
)
|
||||
|
|
|
|||
|
|
@ -152,6 +152,10 @@ class LibraryPreferences(
|
|||
|
||||
fun categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false)
|
||||
|
||||
// KMK -->
|
||||
fun hideHiddenCategories() = preferenceStore.getBoolean("hide_hidden_categories", false)
|
||||
// KMK <--
|
||||
|
||||
fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
||||
|
||||
fun updateCategoriesExclude() = preferenceStore.getStringSet(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
<string name="action_move_bottom">Move bottom</string>
|
||||
<string name="action_confirm_color">Confirm Color</string>
|
||||
<string name="action_remove_merged">Remove merged entries?</string>
|
||||
<string name="action_hide">Hide</string>
|
||||
<string name="action_hide_hidden_categories">Hide hidden categories</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue