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:
Cuong-Tran 2024-09-11 18:06:37 +07:00 committed by GitHub
parent 4a2f8a7895
commit 6c9dfb82f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 201 additions and 13 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,9 @@ class MoveSortingModeSettingsMigration : Migration {
flags = it.flags and 0b00111100L.inv(),
name = null,
order = null,
// KMK -->
hidden = null,
// KMK <--
)
}
}

View file

@ -81,6 +81,9 @@ class Tester {
name = "a",
order = 1,
flags = 0,
// KMK -->
hidden = false,
// KMK <--
),
)
val favoriteEntries = listOf(

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0 NOT NULL;

View file

@ -26,6 +26,9 @@ class CreateCategoryWithName(
name = name,
order = nextOrder,
flags = initialFlags,
// KMK -->
hidden = false,
// KMK <--
)
try {

View file

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

View file

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

View file

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

View file

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

View file

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