From c70338a14d6fe1ed908b916900d4d11ef5e0308c Mon Sep 17 00:00:00 2001 From: Acclorite <2ma0bhpk@anonaddy.com> Date: Mon, 28 Apr 2025 09:18:14 +0300 Subject: [PATCH] feat(library): custom categories * Custom categories * New customization options * Sorting Resolves: #79 --- app/build.gradle.kts | 9 +- .../10.json | 164 ++++++++ .../11.json | 196 ++++++++++ .../12.json | 203 ++++++++++ .../13.json | 235 +++++++++++ .../data/converter/CategoryConverter.kt | 24 ++ .../acclorite/book_story/data/di/AppModule.kt | 1 + .../book_story/data/di/RepositoryModule.kt | 24 ++ .../book_story/data/local/dto/BookEntity.kt | 7 +- .../data/local/dto/CategoryEntity.kt | 18 + .../data/local/dto/CategorySortEntity.kt | 19 + .../book_story/data/local/room/BookDao.kt | 45 +++ .../data/local/room/BookDatabase.kt | 13 +- .../data/mapper/book/BookMapperImpl.kt | 6 +- .../data/mapper/category/CategoryMapper.kt | 16 + .../mapper/category/CategoryMapperImpl.kt | 29 ++ .../category_sort/CategorySortMapper.kt | 16 + .../CategorySortSortMapperImpl.kt | 29 ++ .../data/parser/epub/EpubFileParser.kt | 3 +- .../data/parser/fb2/Fb2FileParser.kt | 3 +- .../data/parser/html/HtmlFileParser.kt | 3 +- .../data/parser/pdf/PdfFileParser.kt | 3 +- .../data/parser/txt/TxtFileParser.kt | 3 +- .../data/repository/CategoryRepositoryImpl.kt | 71 ++++ .../book_story/domain/library/book/Book.kt | 3 +- .../domain/library/category/Category.kt | 11 +- .../{CategoryWithBooks.kt => CategorySort.kt} | 11 +- .../domain/library/display/LibraryLayout.kt | 18 + .../library/display/LibrarySortOrder.kt | 21 + .../library/display/LibraryTitlePosition.kt | 18 + .../domain/repository/CategoryRepository.kt | 38 ++ .../use_case/category/DeleteCategory.kt | 20 + .../domain/use_case/category/GetCategories.kt | 20 + .../use_case/category/GetCategorySort.kt | 20 + .../use_case/category/InsertCategory.kt | 20 + .../use_case/category/UpdateCategoryOrder.kt | 20 + .../use_case/category/UpdateCategorySort.kt | 20 + .../use_case/category/UpdateCategoryTitle.kt | 19 + .../presentation/book_info/BookInfoContent.kt | 9 +- .../presentation/book_info/BookInfoDialog.kt | 7 +- .../book_info/BookInfoMoveDialog.kt | 82 ++-- .../browse/BrowseFilterBottomSheet.kt | 4 +- .../browse/BrowseFilterBottomSheetTabRow.kt | 2 +- .../components/dialog/CheckboxDialogItem.kt | 57 +++ .../core/constants/CommonConstants.kt | 3 +- .../core/constants/DataStoreConstants.kt | 14 + .../core/util/PrimitiveExtensions.kt | 11 + .../library/LibraryBottomSheet.kt | 52 +++ .../presentation/library/LibraryContent.kt | 66 +++- .../presentation/library/LibraryDialog.kt | 11 +- .../library/LibraryFilterBottomSheet.kt | 156 ++++++++ .../library/LibraryFilterBottomSheetTabRow.kt | 30 ++ .../presentation/library/LibraryGridItem.kt | 202 ++++++++++ .../presentation/library/LibraryGridLayout.kt | 44 +++ .../presentation/library/LibraryItem.kt | 164 ++------ .../presentation/library/LibraryLayout.kt | 39 +- .../presentation/library/LibraryListItem.kt | 152 ++++++++ .../presentation/library/LibraryListLayout.kt | 39 ++ .../presentation/library/LibraryMoveDialog.kt | 83 ++-- .../presentation/library/LibraryPager.kt | 165 ++++++-- .../presentation/library/LibraryScaffold.kt | 45 ++- .../presentation/library/LibraryTabs.kt | 34 +- .../presentation/library/LibraryTopBar.kt | 107 ++++- .../presentation/settings/SettingsContent.kt | 2 + .../presentation/settings/SettingsLayout.kt | 13 + .../presentation/settings/SettingsScaffold.kt | 2 + .../settings/browse/BrowseSettingsCategory.kt | 6 +- .../components/BrowseGridSizeOption.kt | 6 +- .../browse/sort/BrowseSortSubcategory.kt | 2 +- .../sort/components/BrowseSortOption.kt | 8 +- .../library/LibrarySettingsCategory.kt | 24 ++ .../library/LibrarySettingsContent.kt | 26 ++ .../settings/library/LibrarySettingsLayout.kt | 30 ++ .../library/LibrarySettingsScaffold.kt | 47 +++ .../settings/library/LibrarySettingsTopBar.kt | 41 ++ .../LibraryCategoriesSubcategory.kt | 41 ++ .../components/LibraryCategoriesOption.kt | 368 ++++++++++++++++++ .../components/LibraryCategoriesOptionNote.kt | 19 + .../display/LibraryDisplaySubcategory.kt | 56 +++ .../components/LibraryGridSizeOption.kt | 42 ++ .../display/components/LibraryLayoutOption.kt | 46 +++ .../components/LibraryReadButtonOption.kt | 34 ++ .../components/LibraryShowProgressOption.kt | 34 ++ .../components/LibraryTitlePositionOption.kt | 59 +++ .../library/sort/LibrarySortSubcategory.kt | 36 ++ .../LibraryPerCategorySortOption.kt | 34 ++ .../sort/components/LibrarySortOption.kt | 95 +++++ .../library/tabs/LibraryTabsSubcategory.kt | 46 +++ .../LibraryAlwaysShowDefaultTabOption.kt | 34 ++ .../components/LibraryShowBookCountOption.kt | 34 ++ .../LibraryShowCategoryTabsOption.kt | 34 ++ .../book_story/ui/book_info/BookInfoEvent.kt | 5 +- .../book_story/ui/book_info/BookInfoModel.kt | 19 +- .../book_story/ui/book_info/BookInfoScreen.kt | 14 +- .../book_story/ui/browse/BrowseModel.kt | 13 +- .../book_story/ui/library/LibraryEvent.kt | 8 +- .../book_story/ui/library/LibraryModel.kt | 29 +- .../book_story/ui/library/LibraryScreen.kt | 70 ++-- .../book_story/ui/library/LibraryState.kt | 4 +- .../acclorite/book_story/ui/main/MainEvent.kt | 12 + .../acclorite/book_story/ui/main/MainModel.kt | 99 +++++ .../acclorite/book_story/ui/main/MainState.kt | 68 ++++ .../ui/settings/LibrarySettingsScreen.kt | 36 ++ .../book_story/ui/settings/SettingsEvent.kt | 25 ++ .../book_story/ui/settings/SettingsModel.kt | 94 ++++- .../book_story/ui/settings/SettingsScreen.kt | 3 + .../book_story/ui/settings/SettingsState.kt | 7 +- .../book_story/ui/theme/ThemeExtensions.kt | 2 - app/src/main/res/values-ar/strings.xml | 28 +- app/src/main/res/values-be/strings.xml | 6 +- app/src/main/res/values-cs/strings.xml | 28 +- app/src/main/res/values-de/strings.xml | 28 +- app/src/main/res/values-es/strings.xml | 28 +- app/src/main/res/values-fr/strings.xml | 28 +- app/src/main/res/values-hi/strings.xml | 28 +- app/src/main/res/values-it/strings.xml | 28 +- app/src/main/res/values-ja/strings.xml | 16 - app/src/main/res/values-ml/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 28 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +- app/src/main/res/values-ro/strings.xml | 15 +- app/src/main/res/values-ta/strings.xml | 28 +- app/src/main/res/values-tr/strings.xml | 34 +- app/src/main/res/values-uk/strings.xml | 76 ++-- app/src/main/res/values-zh-rCN/strings.xml | 28 +- app/src/main/res/values/strings.xml | 80 ++-- 126 files changed, 4521 insertions(+), 722 deletions(-) create mode 100644 app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/10.json create mode 100644 app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/11.json create mode 100644 app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/12.json create mode 100644 app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/13.json create mode 100644 app/src/main/java/ua/acclorite/book_story/data/converter/CategoryConverter.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/local/dto/CategoryEntity.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/local/dto/CategorySortEntity.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapper.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapperImpl.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortMapper.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortSortMapperImpl.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/data/repository/CategoryRepositoryImpl.kt rename app/src/main/java/ua/acclorite/book_story/domain/library/category/{CategoryWithBooks.kt => CategorySort.kt} (53%) create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryLayout.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/library/display/LibrarySortOrder.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryTitlePosition.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/repository/CategoryRepository.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/DeleteCategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategories.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategorySort.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/InsertCategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryOrder.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategorySort.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryTitle.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/CheckboxDialogItem.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBottomSheet.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheet.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheetTabRow.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridItem.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridLayout.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListItem.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListLayout.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsCategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsContent.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsLayout.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsScaffold.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsTopBar.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/LibraryCategoriesSubcategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOptionNote.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/LibraryDisplaySubcategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryGridSizeOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryLayoutOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryReadButtonOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryShowProgressOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryTitlePositionOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/LibrarySortSubcategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibraryPerCategorySortOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibrarySortOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/LibraryTabsSubcategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryAlwaysShowDefaultTabOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowBookCountOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowCategoryTabsOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/ui/settings/LibrarySettingsScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 87bd92df..adeca965 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,9 +136,6 @@ dependencies { // EPUB parser implementation("org.jsoup:jsoup:1.18.3") - // FB2 parser - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.1") - // Language Switcher implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.appcompat:appcompat-resources:1.7.0") @@ -156,9 +153,9 @@ dependencies { // Scrollbar implementation("com.github.nanihadesuka:LazyColumnScrollbar:2.2.0") - // Gson - implementation("com.google.code.gson:gson:2.11.0") - // Markdown implementation("org.commonmark:commonmark:0.24.0") + + // Json + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") } \ No newline at end of file diff --git a/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/10.json b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/10.json new file mode 100644 index 00000000..c1be9420 --- /dev/null +++ b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/10.json @@ -0,0 +1,164 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "1bc94970ad93b16bbf70ab7c0b9cf30a", + "entities": [ + { + "tableName": "BookEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrollIndex", + "columnName": "scrollIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrollOffset", + "columnName": "scrollOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ColorPresetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundColor", + "columnName": "backgroundColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontColor", + "columnName": "fontColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelected", + "columnName": "isSelected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1bc94970ad93b16bbf70ab7c0b9cf30a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/11.json b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/11.json new file mode 100644 index 00000000..96590569 --- /dev/null +++ b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/11.json @@ -0,0 +1,196 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "6e6ecda4455f168e4320d1c7c5d927ed", + "entities": [ + { + "tableName": "BookEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrollIndex", + "columnName": "scrollIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrollOffset", + "columnName": "scrollOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ColorPresetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundColor", + "columnName": "backgroundColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontColor", + "columnName": "fontColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelected", + "columnName": "isSelected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CategoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e6ecda4455f168e4320d1c7c5d927ed')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/12.json b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/12.json new file mode 100644 index 00000000..840a2224 --- /dev/null +++ b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/12.json @@ -0,0 +1,203 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "b48e02ebe9c65bda51e5413bae8308a2", + "entities": [ + { + "tableName": "BookEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `categories` TEXT NOT NULL DEFAULT '[]')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrollIndex", + "columnName": "scrollIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrollOffset", + "columnName": "scrollOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ColorPresetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundColor", + "columnName": "backgroundColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontColor", + "columnName": "fontColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelected", + "columnName": "isSelected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CategoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b48e02ebe9c65bda51e5413bae8308a2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/13.json b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/13.json new file mode 100644 index 00000000..2e8cffbf --- /dev/null +++ b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/13.json @@ -0,0 +1,235 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "7b5d7f69a5a7d3b9878a72578ce34edc", + "entities": [ + { + "tableName": "BookEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `categories` TEXT NOT NULL DEFAULT '[]')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrollIndex", + "columnName": "scrollIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scrollOffset", + "columnName": "scrollOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HistoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookId", + "columnName": "bookId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ColorPresetEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "backgroundColor", + "columnName": "backgroundColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fontColor", + "columnName": "fontColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSelected", + "columnName": "isSelected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CategoryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CategorySortEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`categoryId` INTEGER NOT NULL, `sortOrder` TEXT NOT NULL, `sortOrderDescending` INTEGER NOT NULL, PRIMARY KEY(`categoryId`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sortOrder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortOrderDescending", + "columnName": "sortOrderDescending", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "categoryId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7b5d7f69a5a7d3b9878a72578ce34edc')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/converter/CategoryConverter.kt b/app/src/main/java/ua/acclorite/book_story/data/converter/CategoryConverter.kt new file mode 100644 index 00000000..1402ef42 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/converter/CategoryConverter.kt @@ -0,0 +1,24 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.converter + +import androidx.room.TypeConverter +import kotlinx.serialization.json.Json + +class CategoryConverter { + private val json = Json + + @TypeConverter + fun fromCategoryList(value: List?): String { + return json.encodeToString(value) + } + + @TypeConverter + fun toCategoryList(value: String): List { + return json.decodeFromString(value) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt index a60e5f04..0cf658d8 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt @@ -27,6 +27,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AppModule { + @Provides @Singleton fun provideCommonmarkParser(): Parser { diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt index 05c1d8bb..130a9e70 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt @@ -14,6 +14,10 @@ import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.data_store.DataStoreImpl import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapperImpl +import ua.acclorite.book_story.data.mapper.category.CategoryMapper +import ua.acclorite.book_story.data.mapper.category.CategoryMapperImpl +import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper +import ua.acclorite.book_story.data.mapper.category_sort.CategorySortSortMapperImpl import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl import ua.acclorite.book_story.data.mapper.history.HistoryMapper @@ -23,12 +27,14 @@ import ua.acclorite.book_story.data.parser.FileParserImpl import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParserImpl import ua.acclorite.book_story.data.repository.BookRepositoryImpl +import ua.acclorite.book_story.data.repository.CategoryRepositoryImpl import ua.acclorite.book_story.data.repository.ColorPresetRepositoryImpl import ua.acclorite.book_story.data.repository.DataStoreRepositoryImpl import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl import ua.acclorite.book_story.data.repository.HistoryRepositoryImpl import ua.acclorite.book_story.data.repository.PermissionRepositoryImpl import ua.acclorite.book_story.domain.repository.BookRepository +import ua.acclorite.book_story.domain.repository.CategoryRepository import ua.acclorite.book_story.domain.repository.ColorPresetRepository import ua.acclorite.book_story.domain.repository.DataStoreRepository import ua.acclorite.book_story.domain.repository.FileSystemRepository @@ -81,6 +87,12 @@ abstract class RepositoryModule { permissionRepositoryImpl: PermissionRepositoryImpl ): PermissionRepository + @Binds + @Singleton + abstract fun bindCategoryRepository( + categoryRepositoryImpl: CategoryRepositoryImpl + ): CategoryRepository + @Binds @Singleton abstract fun bindBookMapper( @@ -99,6 +111,18 @@ abstract class RepositoryModule { colorPresetMapperImpl: ColorPresetMapperImpl ): ColorPresetMapper + @Binds + @Singleton + abstract fun bindCategoryMapper( + categoryMapperImpl: CategoryMapperImpl + ): CategoryMapper + + @Binds + @Singleton + abstract fun bindCategorySortMapper( + categorySortSortMapperImpl: CategorySortSortMapperImpl + ): CategorySortMapper + @Binds @Singleton abstract fun bindFileParser( diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt index 201bb01a..1d2ef56e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt @@ -6,11 +6,14 @@ package ua.acclorite.book_story.data.local.dto +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import ua.acclorite.book_story.domain.library.category.Category +import androidx.room.TypeConverters +import ua.acclorite.book_story.data.converter.CategoryConverter @Entity +@TypeConverters(CategoryConverter::class) data class BookEntity( @PrimaryKey(true) val id: Int = 0, val title: String, @@ -21,5 +24,5 @@ data class BookEntity( val scrollOffset: Int, val progress: Float, val image: String? = null, - val category: Category + @ColumnInfo(defaultValue = "[]") val categories: List ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategoryEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategoryEntity.kt new file mode 100644 index 00000000..76a79b50 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategoryEntity.kt @@ -0,0 +1,18 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.local.dto + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class CategoryEntity( + @PrimaryKey(true) + val id: Int = 0, + val title: String, + val order: Int +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategorySortEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategorySortEntity.kt new file mode 100644 index 00000000..f83ea3df --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/CategorySortEntity.kt @@ -0,0 +1,19 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.local.dto + +import androidx.room.Entity +import androidx.room.PrimaryKey +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder + +@Entity +data class CategorySortEntity( + @PrimaryKey(false) + val categoryId: Int, + val sortOrder: LibrarySortOrder, + val sortOrderDescending: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt index 96583f75..66740373 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt @@ -14,6 +14,8 @@ import androidx.room.Query import androidx.room.Update import androidx.room.Upsert import ua.acclorite.book_story.data.local.dto.BookEntity +import ua.acclorite.book_story.data.local.dto.CategoryEntity +import ua.acclorite.book_story.data.local.dto.CategorySortEntity import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity @@ -93,4 +95,47 @@ interface BookDao { @Query("DELETE FROM colorpresetentity") suspend fun deleteColorPresets() /* - - - - - - - - - - - - - - - - - - - - - - */ + + + /* ------ CategoryEntity ----------------- */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCategory( + category: CategoryEntity + ) + + @Query("SELECT * FROM categoryentity") + suspend fun getCategories(): List + + @Query("SELECT COUNT(*) FROM categoryentity") + suspend fun getCategoriesCount(): Int + + @Query("UPDATE categoryentity SET title=:title WHERE id=:id") + suspend fun updateCategoryTitle( + id: Int, + title: String + ) + + @Query("UPDATE categoryentity SET `order`=:order WHERE id=:id") + suspend fun updateCategoryOrder( + id: Int, + order: Int + ) + + @Delete + suspend fun deleteCategory( + category: CategoryEntity + ) + /* - - - - - - - - - - - - - - - - - - - - - - */ + + + /* ------ CategorySortEntity ----------------- */ + @Upsert + suspend fun updateCategorySort(categorySort: CategorySortEntity) + + @Query("SELECT * FROM categorysortentity") + suspend fun getCategorySortEntities(): List + + @Query("DELETE FROM categorysortentity WHERE categoryId=:categoryId") + suspend fun deleteCategorySortEntity(categoryId: Int) + /* - - - - - - - - - - - - - - - - - - - - - - */ } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt index 279db9b6..815f3c3e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt @@ -16,6 +16,8 @@ import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import ua.acclorite.book_story.data.local.dto.BookEntity +import ua.acclorite.book_story.data.local.dto.CategoryEntity +import ua.acclorite.book_story.data.local.dto.CategorySortEntity import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity import java.io.File @@ -25,8 +27,10 @@ import java.io.File BookEntity::class, HistoryEntity::class, ColorPresetEntity::class, + CategoryEntity::class, + CategorySortEntity::class ], - version = 9, + version = 13, autoMigrations = [ AutoMigration(1, 2), AutoMigration(2, 3), @@ -36,6 +40,10 @@ import java.io.File AutoMigration(6, 7), AutoMigration(7, 8, spec = DatabaseHelper.MIGRATION_7_8::class), AutoMigration(8, 9, spec = DatabaseHelper.MIGRATION_8_9::class), + AutoMigration(9, 10, spec = DatabaseHelper.MIGRATION_9_10::class), + AutoMigration(10, 11), + AutoMigration(11, 12), + AutoMigration(12, 13), ], exportSchema = true ) @@ -113,4 +121,7 @@ object DatabaseHelper { @DeleteTable("FavoriteDirectoryEntity") class MIGRATION_8_9 : AutoMigrationSpec + + @DeleteColumn("BookEntity", "category") + class MIGRATION_9_10 : AutoMigrationSpec } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt index 0f2bbad4..e2ceaa20 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt @@ -25,7 +25,7 @@ class BookMapperImpl @Inject constructor() : BookMapper { author = book.author.getAsString(), description = book.description, image = book.coverImage?.toString(), - category = book.category + categories = book.categories ) } @@ -42,8 +42,8 @@ class BookMapperImpl @Inject constructor() : BookMapper { progress = bookEntity.progress, filePath = bookEntity.filePath, lastOpened = null, - category = bookEntity.category, - coverImage = bookEntity.image?.toUri() + coverImage = bookEntity.image?.toUri(), + categories = bookEntity.categories ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapper.kt new file mode 100644 index 00000000..a76be73a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapper.kt @@ -0,0 +1,16 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.mapper.category + +import ua.acclorite.book_story.data.local.dto.CategoryEntity +import ua.acclorite.book_story.domain.library.category.Category + +interface CategoryMapper { + suspend fun toCategoryEntity(category: Category): CategoryEntity + + suspend fun toCategory(categoryEntity: CategoryEntity): Category +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapperImpl.kt new file mode 100644 index 00000000..d0f9238e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/category/CategoryMapperImpl.kt @@ -0,0 +1,29 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.mapper.category + +import ua.acclorite.book_story.data.local.dto.CategoryEntity +import ua.acclorite.book_story.domain.library.category.Category +import javax.inject.Inject + +class CategoryMapperImpl @Inject constructor() : CategoryMapper { + override suspend fun toCategoryEntity(category: Category): CategoryEntity { + return CategoryEntity( + id = category.id, + title = category.title, + order = category.order + ) + } + + override suspend fun toCategory(categoryEntity: CategoryEntity): Category { + return Category( + id = categoryEntity.id, + title = categoryEntity.title, + order = categoryEntity.order + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortMapper.kt new file mode 100644 index 00000000..a0666058 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortMapper.kt @@ -0,0 +1,16 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.mapper.category_sort + +import ua.acclorite.book_story.data.local.dto.CategorySortEntity +import ua.acclorite.book_story.domain.library.category.CategorySort + +interface CategorySortMapper { + suspend fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity + + suspend fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortSortMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortSortMapperImpl.kt new file mode 100644 index 00000000..be5e7c70 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/category_sort/CategorySortSortMapperImpl.kt @@ -0,0 +1,29 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.mapper.category_sort + +import ua.acclorite.book_story.data.local.dto.CategorySortEntity +import ua.acclorite.book_story.domain.library.category.CategorySort +import javax.inject.Inject + +class CategorySortSortMapperImpl @Inject constructor() : CategorySortMapper { + override suspend fun toCategorySortEntity(categorySort: CategorySort): CategorySortEntity { + return CategorySortEntity( + categoryId = categorySort.categoryId, + sortOrder = categorySort.sortOrder, + sortOrderDescending = categorySort.sortOrderDescending + ) + } + + override suspend fun toCategorySort(categorySortEntity: CategorySortEntity): CategorySort { + return CategorySort( + categoryId = categorySortEntity.categoryId, + sortOrder = categorySortEntity.sortOrder, + sortOrderDescending = categorySortEntity.sortOrderDescending + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 9d8f5b0d..7e1a215d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -16,7 +16,6 @@ import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.BookWithCover -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText import java.io.File import java.util.zip.ZipFile @@ -91,7 +90,7 @@ class EpubFileParser @Inject constructor() : FileParser { progress = 0f, filePath = cachedFile.path, lastOpened = null, - category = Category.entries[0], + categories = emptyList(), coverImage = null ), coverImage = extractCoverImageBitmap(rawFile, coverImage) diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt index ffe4caf4..f125bae5 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt @@ -13,7 +13,6 @@ import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.BookWithCover -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText import javax.inject.Inject @@ -56,7 +55,7 @@ class Fb2FileParser @Inject constructor() : FileParser { progress = 0f, filePath = cachedFile.path, lastOpened = null, - category = Category.entries[0], + categories = emptyList(), coverImage = null ), coverImage = null diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt index 3aa8174e..2e9d5266 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt @@ -13,7 +13,6 @@ import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.BookWithCover -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText import javax.inject.Inject @@ -42,7 +41,7 @@ class HtmlFileParser @Inject constructor() : FileParser { progress = 0f, filePath = cachedFile.path, lastOpened = null, - category = Category.entries[0], + categories = emptyList(), coverImage = null ), coverImage = null diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index f4d95ba3..2807ad1f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -14,7 +14,6 @@ import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.BookWithCover -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText import javax.inject.Inject @@ -47,7 +46,7 @@ class PdfFileParser @Inject constructor( progress = 0f, filePath = cachedFile.path, lastOpened = null, - category = Category.entries[0], + categories = emptyList(), coverImage = null ), coverImage = null diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index 32a18202..86620a16 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -11,7 +11,6 @@ import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.file.CachedFile import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.book.BookWithCover -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText import javax.inject.Inject @@ -32,7 +31,7 @@ class TxtFileParser @Inject constructor() : FileParser { progress = 0f, filePath = cachedFile.path, lastOpened = null, - category = Category.entries[0], + categories = emptyList(), coverImage = null ), coverImage = null diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/CategoryRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/CategoryRepositoryImpl.kt new file mode 100644 index 00000000..34f5e1d9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/CategoryRepositoryImpl.kt @@ -0,0 +1,71 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.data.repository + +import ua.acclorite.book_story.data.local.room.BookDao +import ua.acclorite.book_story.data.mapper.category.CategoryMapper +import ua.acclorite.book_story.data.mapper.category_sort.CategorySortMapper +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CategoryRepositoryImpl @Inject constructor( + private val database: BookDao, + private val categoryMapper: CategoryMapper, + private val categorySortMapper: CategorySortMapper, +) : CategoryRepository { + + override suspend fun insertCategory(category: Category) { + database.insertCategory( + categoryMapper.toCategoryEntity( + category.copy( + order = database.getCategoriesCount() + ) + ) + ) + } + + override suspend fun getCategories(): List { + return database.getCategories().map { categoryMapper.toCategory(it) }.sortedBy { it.order } + } + + override suspend fun updateCategoryTitle(id: Int, title: String) { + database.updateCategoryTitle( + id = id, + title = title + ) + updateCategoriesOrder(getCategories()) + } + + override suspend fun updateCategoriesOrder(categories: List) { + categories.mapIndexed { index, category -> + database.updateCategoryOrder(category.id, index) + } + } + + override suspend fun deleteCategory(category: Category) { + database.deleteCategory(categoryMapper.toCategoryEntity(category)) + database.deleteCategorySortEntity(category.id) + + updateCategoriesOrder(getCategories()) + } + + override suspend fun updateCategorySortEntity(categorySort: CategorySort) { + database.updateCategorySort( + categorySortMapper.toCategorySortEntity(categorySort) + ) + } + + override suspend fun getCategorySortEntities(): List { + return database.getCategorySortEntities().map { + categorySortMapper.toCategorySort(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt index ca3b72a5..3919bb1c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/book/Book.kt @@ -10,7 +10,6 @@ import android.net.Uri import android.os.Parcelable import androidx.compose.runtime.Immutable import kotlinx.parcelize.Parcelize -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.ui.UIText @Parcelize @@ -30,5 +29,5 @@ data class Book( val progress: Float, val lastOpened: Long?, - val category: Category + val categories: List ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt index 271b1657..b58c17a9 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/category/Category.kt @@ -9,9 +9,8 @@ package ua.acclorite.book_story.domain.library.category import androidx.compose.runtime.Immutable @Immutable -enum class Category { - READING, - ALREADY_READ, - PLANNING, - DROPPED -} \ No newline at end of file +data class Category( + val id: Int = 0, + val title: String, + val order: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategorySort.kt similarity index 53% rename from app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt rename to app/src/main/java/ua/acclorite/book_story/domain/library/category/CategorySort.kt index 6bfe94e7..20ffca8e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategoryWithBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/category/CategorySort.kt @@ -7,12 +7,11 @@ package ua.acclorite.book_story.domain.library.category import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.domain.library.book.SelectableBook -import ua.acclorite.book_story.domain.ui.UIText +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder @Immutable -data class CategoryWithBooks( - val category: Category, - val title: UIText, - val books: List +data class CategorySort( + val categoryId: Int, + val sortOrder: LibrarySortOrder, + val sortOrderDescending: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryLayout.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryLayout.kt new file mode 100644 index 00000000..36221284 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryLayout.kt @@ -0,0 +1,18 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.library.display + +import androidx.compose.runtime.Immutable + +@Immutable +enum class LibraryLayout { + GRID, LIST +} + +fun String.toLibraryLayout(): LibraryLayout { + return LibraryLayout.valueOf(this) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibrarySortOrder.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibrarySortOrder.kt new file mode 100644 index 00000000..fba9733c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibrarySortOrder.kt @@ -0,0 +1,21 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.library.display + +import androidx.compose.runtime.Immutable + +@Immutable +enum class LibrarySortOrder { + NAME, + LAST_READ, + PROGRESS, + AUTHOR +} + +fun String.toLibrarySortOrder(): LibrarySortOrder { + return LibrarySortOrder.valueOf(this) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryTitlePosition.kt b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryTitlePosition.kt new file mode 100644 index 00000000..d383be87 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/library/display/LibraryTitlePosition.kt @@ -0,0 +1,18 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.library.display + +import androidx.compose.runtime.Immutable + +@Immutable +enum class LibraryTitlePosition { + OFF, BELOW, INSIDE +} + +fun String.toLibraryTitlePosition(): LibraryTitlePosition { + return LibraryTitlePosition.valueOf(this) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/CategoryRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/CategoryRepository.kt new file mode 100644 index 00000000..358dd678 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/CategoryRepository.kt @@ -0,0 +1,38 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.repository + +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort + +interface CategoryRepository { + + suspend fun insertCategory( + category: Category + ) + + suspend fun getCategories(): List + + suspend fun updateCategoryTitle( + id: Int, + title: String + ) + + suspend fun updateCategoriesOrder( + categories: List + ) + + suspend fun deleteCategory( + category: Category + ) + + suspend fun updateCategorySortEntity( + categorySort: CategorySort + ) + + suspend fun getCategorySortEntities(): List +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/DeleteCategory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/DeleteCategory.kt new file mode 100644 index 00000000..fd0cad50 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/DeleteCategory.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class DeleteCategory @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(category: Category) { + repository.deleteCategory(category) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategories.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategories.kt new file mode 100644 index 00000000..3162d0ef --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategories.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class GetCategories @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(): List { + return repository.getCategories() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategorySort.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategorySort.kt new file mode 100644 index 00000000..451c669b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/GetCategorySort.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class GetCategorySort @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(): List { + return repository.getCategorySortEntities() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/InsertCategory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/InsertCategory.kt new file mode 100644 index 00000000..10a5f0f3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/InsertCategory.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class InsertCategory @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(category: Category) { + repository.insertCategory(category) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryOrder.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryOrder.kt new file mode 100644 index 00000000..ea29aac2 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryOrder.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class UpdateCategoryOrder @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(categories: List) { + repository.updateCategoriesOrder(categories) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategorySort.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategorySort.kt new file mode 100644 index 00000000..3216a4a9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategorySort.kt @@ -0,0 +1,20 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class UpdateCategorySort @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(categorySort: CategorySort) { + repository.updateCategorySortEntity(categorySort) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryTitle.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryTitle.kt new file mode 100644 index 00000000..f16cf210 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/category/UpdateCategoryTitle.kt @@ -0,0 +1,19 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.domain.use_case.category + +import ua.acclorite.book_story.domain.repository.CategoryRepository +import javax.inject.Inject + +class UpdateCategoryTitle @Inject constructor( + private val repository: CategoryRepository +) { + + suspend fun execute(id: Int, title: String) { + repository.updateCategoryTitle(id, title) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt index 20bf813a..b388f6a8 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoContent.kt @@ -9,6 +9,7 @@ package ua.acclorite.book_story.presentation.book_info import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.util.BottomSheet import ua.acclorite.book_story.domain.util.Dialog import ua.acclorite.book_story.ui.book_info.BookInfoEvent @@ -16,6 +17,7 @@ import ua.acclorite.book_story.ui.book_info.BookInfoEvent @Composable fun BookInfoContent( book: Book, + categories: List, bottomSheet: BottomSheet?, dialog: Dialog?, listState: LazyListState, @@ -41,12 +43,13 @@ fun BookInfoContent( dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit, navigateToReader: () -> Unit, - navigateToLibrary: () -> Unit, + navigateToLibrarySettings: () -> Unit, navigateBack: () -> Unit ) { BookInfoDialog( dialog = dialog, book = book, + categories = categories, actionTitleDialog = actionTitleDialog, actionAuthorDialog = actionAuthorDialog, actionDescriptionDialog = actionDescriptionDialog, @@ -54,8 +57,8 @@ fun BookInfoContent( actionDeleteDialog = actionDeleteDialog, actionMoveDialog = actionMoveDialog, dismissDialog = dismissDialog, - navigateToLibrary = navigateToLibrary, - navigateBack = navigateBack + navigateBack = navigateBack, + navigateToLibrarySettings = navigateToLibrarySettings ) BookInfoBottomSheet( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt index d07625d5..e9304362 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDialog.kt @@ -8,6 +8,7 @@ package ua.acclorite.book_story.presentation.book_info import androidx.compose.runtime.Composable import ua.acclorite.book_story.domain.library.book.Book +import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.util.Dialog import ua.acclorite.book_story.ui.book_info.BookInfoEvent import ua.acclorite.book_story.ui.book_info.BookInfoScreen @@ -16,6 +17,7 @@ import ua.acclorite.book_story.ui.book_info.BookInfoScreen fun BookInfoDialog( dialog: Dialog?, book: Book, + categories: List, actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit, actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit, actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit, @@ -24,7 +26,7 @@ fun BookInfoDialog( actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, navigateBack: () -> Unit, - navigateToLibrary: () -> Unit + navigateToLibrarySettings: () -> Unit ) { when (dialog) { BookInfoScreen.DELETE_DIALOG -> { @@ -38,9 +40,10 @@ fun BookInfoDialog( BookInfoScreen.MOVE_DIALOG -> { BookInfoMoveDialog( book = book, + categories = categories, actionMoveDialog = actionMoveDialog, dismissDialog = dismissDialog, - navigateToLibrary = navigateToLibrary + navigateToLibrarySettings = navigateToLibrarySettings ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt index 67acc93c..c27d0703 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoMoveDialog.kt @@ -6,69 +6,93 @@ package ua.acclorite.book_story.presentation.book_info +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.DriveFileMove +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.library.book.Book import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.core.components.dialog.CheckboxDialogItem import ua.acclorite.book_story.presentation.core.components.dialog.Dialog -import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem import ua.acclorite.book_story.ui.book_info.BookInfoEvent @Composable fun BookInfoMoveDialog( book: Book, + categories: List, actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit, dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit, - navigateToLibrary: () -> Unit + navigateToLibrarySettings: () -> Unit ) { val context = LocalContext.current - - val categories = remember { - Category.entries.filter { book.category != it } - } - val selectedCategory = remember { - mutableStateOf(categories[0]) + val selectedCategories = remember { + mutableStateListOf().apply { + clear() + categories.forEach { category -> + if (book.categories.any { it == category.id }) { + add(category) + } + } + } } Dialog( - title = stringResource(id = R.string.move_book), - icon = Icons.AutoMirrored.Outlined.DriveFileMove, - description = stringResource( - id = R.string.move_book_description - ), + title = stringResource(id = R.string.move), + description = null, actionEnabled = true, onDismiss = { dismissDialog(BookInfoEvent.OnDismissDialog) }, onAction = { actionMoveDialog( BookInfoEvent.OnActionMoveDialog( - category = selectedCategory.value, - context = context, - navigateToLibrary = navigateToLibrary + selectedCategories = selectedCategories, + context = context ) ) }, + secondaryAction = stringResource(id = R.string.edit), + onSecondaryAction = { + navigateToLibrarySettings() + dismissDialog(BookInfoEvent.OnDismissDialog) + }, withContent = true, items = { - items(categories, key = { it.name }) { - val category = when (it) { - Category.READING -> stringResource(id = R.string.reading_tab) - Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab) - Category.PLANNING -> stringResource(id = R.string.planning_tab) - Category.DROPPED -> stringResource(id = R.string.dropped_tab) + items(categories) { category -> + val selected = remember( + category, + selectedCategories + ) { + derivedStateOf { + selectedCategories.any { it == category } + } } - SelectableDialogItem( - selected = it == selectedCategory.value, - title = category + CheckboxDialogItem( + selected = selected.value, + title = category.title ) { - selectedCategory.value = it + if (selected.value) selectedCategories.remove(category) + else selectedCategories.add(category) + } + } + + if (categories.isEmpty()) { + item { + StyledText( + text = stringResource(id = R.string.categories_empty), + modifier = Modifier.padding(horizontal = 24.dp), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt index a95ecdb9..c8920dc2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheet.kt @@ -57,7 +57,7 @@ fun BrowseFilterBottomSheet( when (page) { 0 -> { LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) { - BrowseFilterSubcategory( + BrowseSortSubcategory( showTitle = false, showDivider = false ) @@ -66,7 +66,7 @@ fun BrowseFilterBottomSheet( 1 -> { LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) { - BrowseSortSubcategory( + BrowseFilterSubcategory( showTitle = false, showDivider = false ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt index eda4e7bc..eccf2e5e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseFilterBottomSheetTabRow.kt @@ -17,8 +17,8 @@ fun BrowseFilterBottomSheetTabRow( scrollToPage: (Int) -> Unit ) { val tabItems = listOf( - stringResource(id = R.string.filter_tab), stringResource(id = R.string.sort_tab), + stringResource(id = R.string.filter_tab), stringResource(id = R.string.display_tab) ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/CheckboxDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/CheckboxDialogItem.kt new file mode 100644 index 00000000..bf1f83b3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/CheckboxDialogItem.kt @@ -0,0 +1,57 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.core.components.dialog + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.presentation.core.components.common.StyledText + +@Composable +fun CheckboxDialogItem( + selected: Boolean, + title: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + } + .padding(start = 12.dp, end = 24.dp) + .padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = selected, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.secondary, + checkmarkColor = MaterialTheme.colorScheme.onSecondary + ), + onCheckedChange = { onClick() } + ) + + Spacer(modifier = Modifier.width(12.dp)) + + StyledText( + text = title, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt index 6051d595..5101a345 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/CommonConstants.kt @@ -10,7 +10,6 @@ package ua.acclorite.book_story.presentation.core.constants import androidx.compose.ui.graphics.Color import ua.acclorite.book_story.domain.library.book.Book -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.domain.ui.UIText @@ -29,7 +28,7 @@ fun provideEmptyBook() = Book( scrollOffset = 0, progress = 0f, lastOpened = null, - category = Category.READING + categories = emptyList() ) // Default Color Preset diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt index 014e99fc..b271fd0f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt @@ -67,6 +67,20 @@ object DataStoreConstants { val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size") val PROGRESS_COUNT = stringPreferencesKey("progress_count") + // Library settings + val LIBRARY_LAYOUT = stringPreferencesKey("library_layout") + val LIBRARY_AUTO_GRID_SIZE = booleanPreferencesKey("library_auto_grid_size") + val LIBRARY_GRID_SIZE = intPreferencesKey("library_grid_size") + val LIBRARY_READ_BUTTON = booleanPreferencesKey("library_read_button") + val LIBRARY_SHOW_PROGRESS = booleanPreferencesKey("library_show_progress") + val LIBRARY_TITLE_POSITION = stringPreferencesKey("library_title_position") + val LIBRARY_SHOW_BOOK_COUNT = booleanPreferencesKey("library_show_book_count") + val LIBRARY_SHOW_CATEGORY_TABS = booleanPreferencesKey("library_show_category_tabs") + val LIBRARY_ALWAYS_SHOW_DEFAULT_TAB = booleanPreferencesKey("library_always_show_default_tab") + val LIBRARY_SORT_ORDER = stringPreferencesKey("library_sort_order") + val LIBRARY_SORT_ORDER_DESCENDING = booleanPreferencesKey("library_sort_order_descending") + val LIBRARY_PER_CATEGORY_SORT = booleanPreferencesKey("library_per_category_sort") + // Browse settings val BROWSE_LAYOUT = stringPreferencesKey("browse_layout") val BROWSE_AUTO_GRID_SIZE = booleanPreferencesKey("browse_auto_grid_size") diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/util/PrimitiveExtensions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/util/PrimitiveExtensions.kt index 65cc4cae..ffdc9fff 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/util/PrimitiveExtensions.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/util/PrimitiveExtensions.kt @@ -65,4 +65,15 @@ fun Char.isVisibleCharacter(): Boolean { else -> false } +} + +fun compareByWithOrder( + descending: Boolean, + selector: (T) -> Comparable<*>? +): Comparator { + return if (descending) { + compareByDescending(selector) + } else { + compareBy(selector) + } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBottomSheet.kt new file mode 100644 index 00000000..7dc1e1b9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryBottomSheet.kt @@ -0,0 +1,52 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.domain.util.BottomSheet +import ua.acclorite.book_story.ui.library.LibraryEvent +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent + +@Composable +fun LibraryBottomSheet( + bottomSheet: BottomSheet?, + categories: List, + showDefaultCategory: Boolean, + pagerState: PagerState, + categoriesSort: List, + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, + perCategorySort: Boolean, + changeLibrarySortOrder: (MainEvent.OnChangeLibrarySortOrder) -> Unit, + changeLibrarySortOrderDescending: (MainEvent.OnChangeLibrarySortOrderDescending) -> Unit, + updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit, + dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit +) { + when (bottomSheet) { + LibraryScreen.FILTER_BOTTOM_SHEET -> { + LibraryFilterBottomSheet( + categories = categories, + showDefaultCategory = showDefaultCategory, + categoriesPagerState = pagerState, + sortOrder = sortOrder, + sortOrderDescending = sortOrderDescending, + categoriesSort = categoriesSort, + perCategorySort = perCategorySort, + changeLibrarySortOrder = changeLibrarySortOrder, + changeLibrarySortOrderDescending = changeLibrarySortOrderDescending, + updateCategorySort = updateCategorySort, + dismissBottomSheet = dismissBottomSheet + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt index 64ade170..debd4aa0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryContent.kt @@ -12,9 +12,16 @@ import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.runtime.Composable import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.library.book.SelectableBook -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition +import ua.acclorite.book_story.domain.util.BottomSheet import ua.acclorite.book_story.domain.util.Dialog import ua.acclorite.book_story.ui.library.LibraryEvent +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent @OptIn(ExperimentalMaterialApi::class) @Composable @@ -22,6 +29,11 @@ fun LibraryContent( books: List, selectedItemsCount: Int, hasSelectedItems: Boolean, + titlePosition: LibraryTitlePosition, + readButton: Boolean, + showProgress: Boolean, + showCategoryTabs: Boolean, + showBookCount: Boolean, showSearch: Boolean, searchQuery: String, bookCount: Int, @@ -30,9 +42,21 @@ fun LibraryContent( isLoading: Boolean, isRefreshing: Boolean, doublePressExit: Boolean, - categories: List, + categories: List, + showDefaultCategory: Boolean, + perCategorySort: Boolean, + categoriesSort: List, + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, + layout: LibraryLayout, + gridSize: Int, + autoGridSize: Boolean, refreshState: PullRefreshState, dialog: Dialog?, + bottomSheet: BottomSheet?, + updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit, + changeLibrarySortOrder: (MainEvent.OnChangeLibrarySortOrder) -> Unit, + changeLibrarySortOrderDescending: (MainEvent.OnChangeLibrarySortOrderDescending) -> Unit, selectBook: (LibraryEvent.OnSelectBook) -> Unit, searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, @@ -44,9 +68,12 @@ fun LibraryContent( actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit, dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit, + showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit, + dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit, navigateToBrowse: () -> Unit, navigateToBookInfo: (id: Int) -> Unit, - navigateToReader: (id: Int) -> Unit + navigateToReader: (id: Int) -> Unit, + navigateToLibrarySettings: () -> Unit ) { LibraryDialog( dialog = dialog, @@ -55,12 +82,34 @@ fun LibraryContent( selectedItemsCount = selectedItemsCount, actionMoveDialog = actionMoveDialog, actionDeleteDialog = actionDeleteDialog, - dismissDialog = dismissDialog + dismissDialog = dismissDialog, + navigateToLibrarySettings = navigateToLibrarySettings + ) + + LibraryBottomSheet( + bottomSheet = bottomSheet, + categories = categories, + showDefaultCategory = showDefaultCategory, + pagerState = pagerState, + categoriesSort = categoriesSort, + sortOrder = sortOrder, + sortOrderDescending = sortOrderDescending, + perCategorySort = perCategorySort, + changeLibrarySortOrder = changeLibrarySortOrder, + changeLibrarySortOrderDescending = changeLibrarySortOrderDescending, + updateCategorySort = updateCategorySort, + dismissBottomSheet = dismissBottomSheet ) LibraryScaffold( + books = books, selectedItemsCount = selectedItemsCount, hasSelectedItems = hasSelectedItems, + titlePosition = titlePosition, + readButton = readButton, + showProgress = showProgress, + showBookCount = showBookCount, + showCategoryTabs = showCategoryTabs, showSearch = showSearch, searchQuery = searchQuery, bookCount = bookCount, @@ -68,7 +117,15 @@ fun LibraryContent( pagerState = pagerState, isLoading = isLoading, isRefreshing = isRefreshing, + layout = layout, + gridSize = gridSize, + autoGridSize = autoGridSize, categories = categories, + showDefaultCategory = showDefaultCategory, + perCategorySort = perCategorySort, + categoriesSort = categoriesSort, + sortOrder = sortOrder, + sortOrderDescending = sortOrderDescending, searchVisibility = searchVisibility, requestFocus = requestFocus, searchQueryChange = searchQueryChange, @@ -77,6 +134,7 @@ fun LibraryContent( clearSelectedBooks = clearSelectedBooks, showMoveDialog = showMoveDialog, showDeleteDialog = showDeleteDialog, + showFilterBottomSheet = showFilterBottomSheet, refreshState = refreshState, navigateToBrowse = navigateToBrowse, navigateToBookInfo = navigateToBookInfo, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt index 2860422c..b90d147b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryDialog.kt @@ -8,7 +8,7 @@ package ua.acclorite.book_story.presentation.library import androidx.compose.runtime.Composable import ua.acclorite.book_story.domain.library.book.SelectableBook -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.util.Dialog import ua.acclorite.book_story.ui.library.LibraryEvent import ua.acclorite.book_story.ui.library.LibraryScreen @@ -17,20 +17,21 @@ import ua.acclorite.book_story.ui.library.LibraryScreen fun LibraryDialog( dialog: Dialog?, books: List, - categories: List, + categories: List, selectedItemsCount: Int, actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit, - dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit, + navigateToLibrarySettings: () -> Unit ) { when (dialog) { LibraryScreen.MOVE_DIALOG -> { LibraryMoveDialog( books = books, categories = categories, - selectedItemsCount = selectedItemsCount, actionMoveDialog = actionMoveDialog, - dismissDialog = dismissDialog + dismissDialog = dismissDialog, + navigateToLibrarySettings = navigateToLibrarySettings ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheet.kt new file mode 100644 index 00000000..881fca21 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheet.kt @@ -0,0 +1,156 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet +import ua.acclorite.book_story.presentation.settings.library.display.LibraryDisplaySubcategory +import ua.acclorite.book_story.presentation.settings.library.sort.components.LibrarySortOption +import ua.acclorite.book_story.presentation.settings.library.tabs.LibraryTabsSubcategory +import ua.acclorite.book_story.ui.library.LibraryEvent +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.settings.SettingsEvent + +private var initialPage = 0 + +@Composable +fun LibraryFilterBottomSheet( + categories: List, + showDefaultCategory: Boolean, + categoriesPagerState: PagerState, + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, + categoriesSort: List, + perCategorySort: Boolean, + changeLibrarySortOrder: (MainEvent.OnChangeLibrarySortOrder) -> Unit, + changeLibrarySortOrderDescending: (MainEvent.OnChangeLibrarySortOrderDescending) -> Unit, + updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit, + dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit +) { + val currentCategory = remember( + categories, + showDefaultCategory, + categoriesPagerState.currentPage + ) { + derivedStateOf { + categories.toMutableList().apply { + if (showDefaultCategory) { + add(0, Category(id = -1, title = "")) + } + }[categoriesPagerState.currentPage] + } + } + + val currentCategorySort = remember(currentCategory, categoriesSort) { + derivedStateOf { + categoriesSort.firstOrNull { it.categoryId == currentCategory.value.id } + ?: CategorySort( + categoryId = currentCategory.value.id, + sortOrder = LibrarySortOrder.LAST_READ, + sortOrderDescending = true + ) + } + } + + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage) { 2 } + DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } } + + ModalBottomSheet( + hasFixedHeight = true, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.5f), + dragHandle = {}, + onDismissRequest = { + dismissBottomSheet(LibraryEvent.OnDismissBottomSheet) + }, + sheetGesturesEnabled = false + ) { + LibraryFilterBottomSheetTabRow( + currentPage = pagerState.currentPage, + scrollToPage = { + scope.launch { + pagerState.animateScrollToPage(it) + } + } + ) + + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> + when (page) { + 0 -> { + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(top = 18.dp) + ) { + LibrarySortOption( + sortOrder = if (perCategorySort) { + currentCategorySort.value.sortOrder + } else sortOrder, + sortOrderDescending = if (perCategorySort) { + currentCategorySort.value.sortOrderDescending + } else sortOrderDescending, + onChange = { sortOrder, sortOrderDescending -> + if (perCategorySort) { + updateCategorySort( + SettingsEvent.OnUpdateCategorySort( + categoryId = currentCategorySort.value.categoryId, + sortOrder = sortOrder, + sortOrderDescending = sortOrderDescending + ) + ) + } else { + changeLibrarySortOrder( + MainEvent.OnChangeLibrarySortOrder( + sortOrder.name + ) + ) + changeLibrarySortOrderDescending( + MainEvent.OnChangeLibrarySortOrderDescending( + sortOrderDescending + ) + ) + } + } + ) + } + } + + 1 -> { + LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) { + LibraryDisplaySubcategory( + showTitle = false + ) + LibraryTabsSubcategory( + titleColor = { MaterialTheme.colorScheme.onSurface }, + showDivider = false + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheetTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheetTabRow.kt new file mode 100644 index 00000000..fecfd70f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryFilterBottomSheetTabRow.kt @@ -0,0 +1,30 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheetTabRow + +@Composable +fun LibraryFilterBottomSheetTabRow( + currentPage: Int, + scrollToPage: (Int) -> Unit +) { + val tabItems = listOf( + stringResource(id = R.string.sort_tab), + stringResource(id = R.string.display_tab), + ) + + ModalBottomSheetTabRow( + selectedTabIndex = currentPage, + tabs = tabItems + ) { index -> + scrollToPage(index) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridItem.kt new file mode 100644 index 00000000..94fca0f3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridItem.kt @@ -0,0 +1,202 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition +import ua.acclorite.book_story.presentation.core.components.common.AsyncCoverImage +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.core.util.calculateProgress + +@Composable +fun LibraryGridItem( + book: SelectableBook, + hasSelectedItems: Boolean, + titlePosition: LibraryTitlePosition, + readButton: Boolean, + showProgress: Boolean, + selectBook: (select: Boolean?) -> Unit, + navigateToBookInfo: () -> Unit, + navigateToReader: () -> Unit +) { + val backgroundColor = if (book.selected) MaterialTheme.colorScheme.secondary + else Color.Transparent + val belowFontColor = if (book.selected) MaterialTheme.colorScheme.onSecondary + else MaterialTheme.colorScheme.onSurface + + val scrimColor = if (titlePosition == LibraryTitlePosition.INSIDE) { + MaterialTheme.colorScheme.scrim.copy(0.3f) + } else Color.Transparent + val insideFontColor = Color.White.copy(0.85f) + + val progress = rememberSaveable(book.data.progress) { + "${book.data.progress.calculateProgress(1)}%" + } + + Column( + Modifier + .padding(3.dp) + .clip(MaterialTheme.shapes.large) + .background(backgroundColor) + .padding(3.dp) + ) { + Box( + modifier = Modifier + .aspectRatio(1f / 1.5f) + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .combinedClickable( + onClick = { + if (hasSelectedItems) selectBook(null) + else navigateToBookInfo() + }, + onLongClick = { + if (!hasSelectedItems) selectBook(true) + } + ) + ) { + if (book.data.coverImage != null) { + AsyncCoverImage( + uri = book.data.coverImage, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium) + ) + } else { + Icon( + imageVector = Icons.Default.Image, + contentDescription = stringResource( + id = R.string.cover_image_not_found_content_desc + ), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.colorScheme.surfaceContainerHigh + ) + } + + if (showProgress) { + StyledText( + text = progress, + modifier = Modifier + .padding(6.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 6.dp, vertical = 3.dp), + style = MaterialTheme.typography.bodySmall.copy( + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onTertiary, + ) + ) + } + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .drawWithContent { + drawRect( + brush = Brush.verticalGradient( + 0f to Color.Transparent, + 1f to scrimColor + ) + ) + drawContent() + } + .padding(6.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + if (titlePosition == LibraryTitlePosition.INSIDE) { + StyledText( + text = book.data.title, + modifier = Modifier + .weight(1f) + .padding(horizontal = 2.dp), + style = MaterialTheme.typography.bodySmall.copy( + color = insideFontColor + ), + maxLines = 2 + ) + + Spacer(modifier = Modifier.width(6.dp)) + } + + if (readButton) { + FilledIconButton( + onClick = { + if (readButton) navigateToReader() + }, + modifier = Modifier.size(32.dp), + shape = MaterialTheme.shapes.medium, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.continue_reading_content_desc), + Modifier.size(20.dp) + ) + } + } + } + } + + if (titlePosition == LibraryTitlePosition.BELOW) { + Spacer(modifier = Modifier.height(6.dp)) + StyledText( + text = book.data.title, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + style = MaterialTheme.typography.bodyMedium.copy( + color = belowFontColor + ), + minLines = 2, + maxLines = 2 + ) + Spacer(modifier = Modifier.height(2.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridLayout.kt new file mode 100644 index 00000000..88560b8b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryGridLayout.kt @@ -0,0 +1,44 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar + +@Composable +fun LibraryGridLayout( + books: List, + gridSize: Int, + autoGridSize: Boolean, + itemContent: @Composable (book: SelectableBook) -> Unit +) { + LazyVerticalGridWithScrollbar( + columns = if (autoGridSize) GridCells.Adaptive(120.dp) + else GridCells.Fixed(gridSize.coerceAtLeast(1)), + modifier = Modifier.fillMaxSize(), + scrollbarSettings = providePrimaryScrollbar(false), + contentPadding = PaddingValues(8.dp) + ) { + items( + books, + key = { it.data.id } + ) { book -> + Box(modifier = Modifier.animateItem()) { + itemContent(book) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt index 5763ca84..00e4a242 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryItem.kt @@ -6,151 +6,47 @@ package ua.acclorite.book_story.presentation.library -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.LazyGridItemScope -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.library.book.SelectableBook -import ua.acclorite.book_story.presentation.core.components.common.AsyncCoverImage -import ua.acclorite.book_story.presentation.core.components.common.StyledText -import ua.acclorite.book_story.presentation.core.util.calculateProgress +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition @Composable -fun LazyGridItemScope.LibraryItem( +fun LibraryItem( book: SelectableBook, + layout: LibraryLayout, hasSelectedItems: Boolean, + titlePosition: LibraryTitlePosition, + readButton: Boolean, + showProgress: Boolean, selectBook: (select: Boolean?) -> Unit, navigateToBookInfo: () -> Unit, navigateToReader: () -> Unit ) { - val backgroundColor = if (book.selected) MaterialTheme.colorScheme.secondary - else Color.Transparent - val fontColor = if (book.selected) MaterialTheme.colorScheme.onSecondary - else MaterialTheme.colorScheme.onSurface - - val progress = rememberSaveable(book.data.progress) { - "${book.data.progress.calculateProgress(1)}%" - } - - Column( - Modifier - .animateItem() - .padding(3.dp) - .background( - backgroundColor, - MaterialTheme.shapes.large + when (layout) { + LibraryLayout.LIST -> { + LibraryListItem( + book = book, + hasSelectedItems = hasSelectedItems, + readButton = readButton, + showProgress = showProgress, + selectBook = selectBook, + navigateToBookInfo = navigateToBookInfo, + navigateToReader = navigateToReader + ) + } + + LibraryLayout.GRID -> { + LibraryGridItem( + book = book, + hasSelectedItems = hasSelectedItems, + titlePosition = titlePosition, + readButton = readButton, + showProgress = showProgress, + selectBook = selectBook, + navigateToBookInfo = navigateToBookInfo, + navigateToReader = navigateToReader ) - .padding(3.dp) - .padding(bottom = 2.dp) - ) { - Box( - modifier = Modifier - .aspectRatio(1f / 1.5f) - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background( - MaterialTheme.colorScheme.surfaceContainerLow, - MaterialTheme.shapes.medium - ) - .combinedClickable( - onClick = { - if (hasSelectedItems) selectBook(null) - else navigateToBookInfo() - }, - onLongClick = { - if (!hasSelectedItems) selectBook(true) - } - ) - ) { - if (book.data.coverImage != null) { - AsyncCoverImage( - uri = book.data.coverImage, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium) - ) - } else { - Icon( - imageVector = Icons.Default.Image, - contentDescription = stringResource( - id = R.string.cover_image_not_found_content_desc - ), - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(0.7f) - .aspectRatio(1f), - tint = MaterialTheme.colorScheme.surfaceContainerHigh - ) - } - - StyledText( - text = progress, - modifier = Modifier - .padding(6.dp) - .clip(MaterialTheme.shapes.small) - .background(MaterialTheme.colorScheme.secondary, MaterialTheme.shapes.small) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.bodySmall.copy( - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSecondary, - ) - ) - - FilledIconButton( - onClick = { navigateToReader() }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(6.dp) - .size(32.dp), - shape = MaterialTheme.shapes.medium, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) { - Icon( - imageVector = Icons.Filled.PlayArrow, - contentDescription = stringResource(id = R.string.continue_reading_content_desc), - Modifier.size(20.dp) - ) - } } - Spacer(modifier = Modifier.height(6.dp)) - StyledText( - text = book.data.title, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp), - style = MaterialTheme.typography.bodyMedium.copy( - color = fontColor - ), - minLines = 2, - maxLines = 2 - ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt index 7ece00f7..f3579895 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryLayout.kt @@ -6,26 +6,33 @@ package ua.acclorite.book_story.presentation.library -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar -import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.display.LibraryLayout @Composable fun LibraryLayout( - items: LazyGridScope.() -> Unit + books: List, + layout: LibraryLayout, + gridSize: Int, + autoGridSize: Boolean, + itemContent: @Composable (book: SelectableBook) -> Unit ) { - LazyVerticalGridWithScrollbar( - columns = GridCells.Adaptive(120.dp), - modifier = Modifier.fillMaxSize(), - scrollbarSettings = providePrimaryScrollbar(false), - contentPadding = PaddingValues(8.dp) - ) { - items() + when (layout) { + LibraryLayout.LIST -> { + LibraryListLayout( + books = books, + itemContent = itemContent + ) + } + + LibraryLayout.GRID -> { + LibraryGridLayout( + books = books, + gridSize = gridSize, + autoGridSize = autoGridSize, + itemContent = itemContent + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListItem.kt new file mode 100644 index 00000000..46852095 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListItem.kt @@ -0,0 +1,152 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.presentation.core.components.common.AsyncCoverImage +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.core.util.calculateProgress + +@Composable +fun LibraryListItem( + book: SelectableBook, + hasSelectedItems: Boolean, + readButton: Boolean, + showProgress: Boolean, + selectBook: (select: Boolean?) -> Unit, + navigateToBookInfo: () -> Unit, + navigateToReader: () -> Unit +) { + val backgroundColor = if (book.selected) MaterialTheme.colorScheme.secondaryContainer + else Color.Transparent + val fontColor = if (book.selected) MaterialTheme.colorScheme.onSecondaryContainer + else MaterialTheme.colorScheme.onSurface + + val progress = rememberSaveable(book.data.progress) { + "${book.data.progress.calculateProgress(1)}%" + } + + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 3.dp) + .clip(MaterialTheme.shapes.medium) + .background(backgroundColor) + .combinedClickable( + onClick = { + if (hasSelectedItems) selectBook(null) + else navigateToBookInfo() + }, + onLongClick = { + if (!hasSelectedItems) selectBook(true) + } + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(50.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + ) { + if (book.data.coverImage != null) { + AsyncCoverImage( + uri = book.data.coverImage, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.small) + ) + } else { + Icon( + imageVector = Icons.Default.Image, + contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(0.7f) + .aspectRatio(1f), + tint = MaterialTheme.colorScheme.surfaceContainerHigh + ) + } + } + + Spacer(modifier = Modifier.width(16.dp)) + + StyledText( + text = book.data.title, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium.copy( + color = fontColor + ), + maxLines = 2 + ) + + Spacer(modifier = Modifier.width(8.dp)) + + if (showProgress) { + Spacer(modifier = Modifier.width(8.dp)) + + StyledText( + text = progress, + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.tertiary) + .padding(horizontal = 6.dp, vertical = 3.dp), + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onTertiary + ) + ) + } + + if (readButton) { + Spacer(modifier = Modifier.width(8.dp)) + + FilledIconButton( + onClick = { if (readButton) navigateToReader() }, + modifier = Modifier.size(32.dp), + shape = MaterialTheme.shapes.medium, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.continue_reading_content_desc), + modifier = Modifier.size(20.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListLayout.kt new file mode 100644 index 00000000..6f4d8982 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryListLayout.kt @@ -0,0 +1,39 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.library + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar +import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar + +@Composable +fun LibraryListLayout( + books: List, + itemContent: @Composable (book: SelectableBook) -> Unit +) { + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + scrollbarSettings = providePrimaryScrollbar(false), + contentPadding = PaddingValues(8.dp) + ) { + items( + books, + key = { it.data.id } + ) { book -> + Box(modifier = Modifier.animateItem()) { + itemContent(book) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt index cc5a5ad4..b6958546 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryMoveDialog.kt @@ -6,57 +6,54 @@ package ua.acclorite.book_story.presentation.library +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MoveUp +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.library.book.SelectableBook -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.core.components.dialog.CheckboxDialogItem import ua.acclorite.book_story.presentation.core.components.dialog.Dialog -import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem import ua.acclorite.book_story.ui.library.LibraryEvent @Composable fun LibraryMoveDialog( books: List, - categories: List, - selectedItemsCount: Int, + categories: List, actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit, - dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit + dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit, + navigateToLibrarySettings: () -> Unit ) { val context = LocalContext.current + val selectedBooks = remember { derivedStateOf { books.filter { it.selected } } } - val moveCategories = remember { - derivedStateOf { - categories.mapNotNull { category -> - if (!selectedBooks.value.all { it.data.category == category.category }) { - return@mapNotNull category + val selectedCategories = remember { + mutableStateListOf().apply { + clear() + categories.forEach { category -> + if (selectedBooks.value.all { it.data.categories.any { it == category.id } }) { + add(category) } - return@mapNotNull null } } } - val selectedCategory = remember { - mutableStateOf(moveCategories.value[0]) - } Dialog( - title = stringResource(id = R.string.move_books), - icon = Icons.Outlined.MoveUp, - description = stringResource( - id = R.string.move_books_description, - selectedItemsCount - ), + title = stringResource(id = R.string.move), + description = null, actionEnabled = true, onDismiss = { dismissDialog(LibraryEvent.OnDismissDialog) @@ -64,20 +61,46 @@ fun LibraryMoveDialog( onAction = { actionMoveDialog( LibraryEvent.OnActionMoveDialog( - selectedCategory = selectedCategory.value.category, - categories = categories, + selectedCategories = selectedCategories, context = context ) ) }, + secondaryAction = stringResource(id = R.string.edit), + onSecondaryAction = { + navigateToLibrarySettings() + dismissDialog(LibraryEvent.OnDismissDialog) + }, withContent = true, items = { - items(moveCategories.value) { category -> - SelectableDialogItem( - selected = category == selectedCategory.value, - title = category.title.asString() + items(categories) { category -> + val selected = remember( + category, + selectedCategories ) { - selectedCategory.value = category + derivedStateOf { + selectedCategories.any { it == category } + } + } + + CheckboxDialogItem( + selected = selected.value, + title = category.title + ) { + if (selected.value) selectedCategories.remove(category) + else selectedCategories.add(category) + } + } + + if (categories.isEmpty()) { + item { + StyledText( + text = stringResource(id = R.string.categories_empty), + modifier = Modifier.padding(horizontal = 24.dp), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt index 324a6c9b..0a73826c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryPager.kt @@ -8,22 +8,39 @@ package ua.acclorite.book_story.presentation.library import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition +import ua.acclorite.book_story.presentation.core.util.compareByWithOrder import ua.acclorite.book_story.ui.library.LibraryEvent import ua.acclorite.book_story.ui.theme.DefaultTransition @Composable fun LibraryPager( + books: List, pagerState: PagerState, - categories: List, + categories: List, + showDefaultCategory: Boolean, + perCategorySort: Boolean, + categoriesSort: List, + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, + layout: LibraryLayout, + gridSize: Int, + autoGridSize: Boolean, hasSelectedItems: Boolean, + titlePosition: LibraryTitlePosition, + readButton: Boolean, + showProgress: Boolean, isLoading: Boolean, isRefreshing: Boolean, selectBook: (LibraryEvent.OnSelectBook) -> Unit, @@ -31,42 +48,140 @@ fun LibraryPager( navigateToBookInfo: (id: Int) -> Unit, navigateToReader: (id: Int) -> Unit, ) { + val categorizedBooks = remember( + books, + categories, + categoriesSort, + perCategorySort, + sortOrder, + sortOrderDescending, + showDefaultCategory + ) { + fun List.sortBooks( + librarySortOrder: LibrarySortOrder, + librarySortOrderDescending: Boolean + ): List { + return sortedWith( + compareByWithOrder(librarySortOrderDescending) { book -> + when (librarySortOrder) { + LibrarySortOrder.NAME -> book.data.title.trim() + LibrarySortOrder.LAST_READ -> book.data.lastOpened + LibrarySortOrder.PROGRESS -> book.data.progress + LibrarySortOrder.AUTHOR -> book.data.author.getAsString() + } + } + ) + } + + derivedStateOf { + val categorizedBooks = mutableListOf>() + + if (showDefaultCategory) { + val categoryIds = categories.map { it.id }.toSet() + categorizedBooks.add( + books.filter { book -> + book.data.categories.none { category -> category in categoryIds } + }.let { + if (perCategorySort) { + val categorySort = categoriesSort.firstOrNull { + it.categoryId == -1 + } ?: CategorySort( + categoryId = -1, + sortOrder = LibrarySortOrder.LAST_READ, + sortOrderDescending = true + ) + + return@let it.sortBooks( + categorySort.sortOrder, + categorySort.sortOrderDescending + ) + } + + return@let it + } + ) + } + + categories.sortedBy { it.order }.forEach { category -> + categorizedBooks.add( + books + .filter { it.data.categories.any { category.id == it } } + .let { + if (perCategorySort) { + val categorySort = categoriesSort.firstOrNull { + it.categoryId == category.id + } ?: CategorySort( + categoryId = category.id, + sortOrder = LibrarySortOrder.LAST_READ, + sortOrderDescending = true + ) + + return@let it.sortBooks( + categorySort.sortOrder, + categorySort.sortOrderDescending + ) + } + + return@let it + } + ) + } + + return@derivedStateOf categorizedBooks.let { + if (!perCategorySort) { + return@let it.map { + it.sortBooks( + sortOrder, + sortOrderDescending + ) + } + } + + return@let it + } + } + } + HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { index -> - val category = remember(categories, index) { + val category = remember(categorizedBooks, index) { derivedStateOf { - categories[index] + categorizedBooks.value[index] } } Box(modifier = Modifier.fillMaxSize()) { DefaultTransition(visible = !isLoading) { - LibraryLayout { - items( - category.value.books, - key = { it.data.id } - ) { book -> - LibraryItem( - book = book, - hasSelectedItems = hasSelectedItems, - selectBook = { select -> - selectBook( - LibraryEvent.OnSelectBook( - id = book.data.id, - select = select - ) + LibraryLayout( + books = category.value, + gridSize = gridSize, + autoGridSize = autoGridSize, + layout = layout + ) { book -> + LibraryItem( + book = book, + layout = layout, + hasSelectedItems = hasSelectedItems, + titlePosition = titlePosition, + readButton = readButton, + showProgress = showProgress, + selectBook = { select -> + selectBook( + LibraryEvent.OnSelectBook( + id = book.data.id, + select = select ) - }, - navigateToBookInfo = { navigateToBookInfo(book.data.id) }, - navigateToReader = { navigateToReader(book.data.id) }, - ) - } + ) + }, + navigateToBookInfo = { navigateToBookInfo(book.data.id) }, + navigateToReader = { navigateToReader(book.data.id) }, + ) } } LibraryEmptyPlaceholder( isLoading = isLoading, isRefreshing = isRefreshing, - isBooksEmpty = category.value.books.isEmpty(), + isBooksEmpty = category.value.isEmpty(), navigateToBrowse = navigateToBrowse ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt index 2934ece2..3ce87923 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryScaffold.kt @@ -18,14 +18,25 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition import ua.acclorite.book_story.ui.library.LibraryEvent @OptIn(ExperimentalMaterialApi::class) @Composable fun LibraryScaffold( + books: List, selectedItemsCount: Int, hasSelectedItems: Boolean, + titlePosition: LibraryTitlePosition, + readButton: Boolean, + showProgress: Boolean, + showCategoryTabs: Boolean, + showBookCount: Boolean, showSearch: Boolean, searchQuery: String, bookCount: Int, @@ -34,7 +45,15 @@ fun LibraryScaffold( isLoading: Boolean, isRefreshing: Boolean, refreshState: PullRefreshState, - categories: List, + layout: LibraryLayout, + gridSize: Int, + autoGridSize: Boolean, + categories: List, + showDefaultCategory: Boolean, + perCategorySort: Boolean, + categoriesSort: List, + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, selectBook: (LibraryEvent.OnSelectBook) -> Unit, requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, @@ -43,6 +62,7 @@ fun LibraryScaffold( clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit, showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit, + showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit, navigateToBrowse: () -> Unit, navigateToBookInfo: (id: Int) -> Unit, navigateToReader: (id: Int) -> Unit, @@ -54,8 +74,11 @@ fun LibraryScaffold( containerColor = MaterialTheme.colorScheme.surface, topBar = { LibraryTopBar( + books = books, selectedItemsCount = selectedItemsCount, hasSelectedItems = hasSelectedItems, + showBookCount = showBookCount, + showCategoryTabs = showCategoryTabs, showSearch = showSearch, searchQuery = searchQuery, bookCount = bookCount, @@ -64,13 +87,15 @@ fun LibraryScaffold( isLoading = isLoading, isRefreshing = isRefreshing, categories = categories, + showDefaultCategory = showDefaultCategory, searchVisibility = searchVisibility, requestFocus = requestFocus, searchQueryChange = searchQueryChange, search = search, clearSelectedBooks = clearSelectedBooks, showMoveDialog = showMoveDialog, - showDeleteDialog = showDeleteDialog + showDeleteDialog = showDeleteDialog, + showFilterBottomSheet = showFilterBottomSheet ) } ) { paddingValues -> @@ -80,15 +105,27 @@ fun LibraryScaffold( .padding(top = paddingValues.calculateTopPadding()) ) { LibraryPager( + books = books, pagerState = pagerState, categories = categories, + showDefaultCategory = showDefaultCategory, + perCategorySort = perCategorySort, + categoriesSort = categoriesSort, + sortOrder = sortOrder, + sortOrderDescending = sortOrderDescending, + layout = layout, + gridSize = gridSize, + autoGridSize = autoGridSize, hasSelectedItems = hasSelectedItems, + titlePosition = titlePosition, + readButton = readButton, + showProgress = showProgress, isLoading = isLoading, isRefreshing = isRefreshing, selectBook = selectBook, navigateToBrowse = navigateToBrowse, navigateToReader = navigateToReader, - navigateToBookInfo = navigateToBookInfo, + navigateToBookInfo = navigateToBookInfo ) LibraryRefreshIndicator( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt index c2ead32a..ce84456c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTabs.kt @@ -29,14 +29,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.presentation.core.components.common.StyledText @Composable fun LibraryTabs( - categories: List, + categoriesWithBookCount: List>, pagerState: PagerState, itemCountBackgroundColor: Color, + showBookCount: Boolean ) { val scope = rememberCoroutineScope() @@ -69,7 +70,7 @@ fun LibraryTabs( } } ) { - categories.forEachIndexed { index, tabItem -> + categoriesWithBookCount.forEachIndexed { index, tabItem -> Tab( selected = pagerState.currentPage == index, onClick = { @@ -82,23 +83,26 @@ fun LibraryTabs( text = { Row(verticalAlignment = Alignment.CenterVertically) { StyledText( - text = tabItem.title.asString(), + text = tabItem.first.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, ) - Spacer(modifier = Modifier.width(6.dp)) - StyledText( - text = tabItem.books.count().toString(), - modifier = Modifier - .background( - itemCountBackgroundColor, - MaterialTheme.shapes.medium + + if (showBookCount) { + Spacer(modifier = Modifier.width(6.dp)) + StyledText( + text = tabItem.second.toString(), + modifier = Modifier + .background( + itemCountBackgroundColor, + MaterialTheme.shapes.medium + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant ) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant ) - ) + } } } ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt index 37ee888a..3093a44a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/library/LibraryTopBar.kt @@ -9,6 +9,8 @@ package ua.acclorite.book_story.presentation.library import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,6 +21,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.MoveUp @@ -26,6 +29,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -35,7 +40,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks +import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility import ua.acclorite.book_story.presentation.core.components.common.IconButton import ua.acclorite.book_story.presentation.core.components.common.SearchTextField import ua.acclorite.book_story.presentation.core.components.common.StyledText @@ -47,8 +54,11 @@ import ua.acclorite.book_story.ui.library.LibraryEvent @OptIn(ExperimentalMaterial3Api::class) @Composable fun LibraryTopBar( + books: List, selectedItemsCount: Int, hasSelectedItems: Boolean, + showBookCount: Boolean, + showCategoryTabs: Boolean, showSearch: Boolean, searchQuery: String, bookCount: Int, @@ -56,15 +66,49 @@ fun LibraryTopBar( pagerState: PagerState, isLoading: Boolean, isRefreshing: Boolean, - categories: List, + categories: List, + showDefaultCategory: Boolean, searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit, requestFocus: (LibraryEvent.OnRequestFocus) -> Unit, searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit, search: (LibraryEvent.OnSearch) -> Unit, clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit, showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit, - showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit + showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit, + showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit ) { + val defaultCategory = stringResource(id = R.string.default_tab) + val categoriesWithBookCount = remember( + books, + categories, + showDefaultCategory, + defaultCategory + ) { + derivedStateOf { + categories.map { category -> + category to books.count { it.data.categories.any { it == category.id } } + }.toMutableList().apply { + if (showDefaultCategory) { + val categoryIds = categories.map { it.id }.toSet() + add( + 0, + Category( + id = -1, + title = defaultCategory + ) to books.count { book -> + book.data.categories.none { category -> category in categoryIds } + } + ) + } + }.toList() + } + } + val currentCategory = remember(pagerState.currentPage, categoriesWithBookCount) { + derivedStateOf { + categoriesWithBookCount.value[pagerState.currentPage] + } + } + val animatedItemCountBackgroundColor = animateColorAsState( if (hasSelectedItems) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surfaceContainer, @@ -86,21 +130,28 @@ fun LibraryTopBar( contentNavigationIcon = {}, contentTitle = { Row(verticalAlignment = Alignment.CenterVertically) { - StyledText(text = stringResource(id = R.string.library_screen)) - Spacer(modifier = Modifier.width(6.dp)) StyledText( - text = bookCount.toString(), - modifier = Modifier - .background( - MaterialTheme.colorScheme.surfaceContainer, - RoundedCornerShape(14.dp) - ) - .padding(horizontal = 8.dp, vertical = 3.dp), - style = LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 16.sp - ) + text = if (showCategoryTabs) stringResource(id = R.string.library_screen) + else currentCategory.value.first.title ) + + if (showBookCount) { + Spacer(modifier = Modifier.width(6.dp)) + StyledText( + text = if (showCategoryTabs) bookCount.toString() + else currentCategory.value.second.toString(), + modifier = Modifier + .background( + MaterialTheme.colorScheme.surfaceContainer, + RoundedCornerShape(14.dp) + ) + .padding(horizontal = 8.dp, vertical = 3.dp), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 16.sp + ) + ) + } } }, contentActions = { @@ -111,6 +162,13 @@ fun LibraryTopBar( ) { searchVisibility(LibraryEvent.OnSearchVisibility(true)) } + IconButton( + icon = Icons.Default.FilterList, + contentDescription = R.string.filter_content_desc, + disableOnClick = false, + ) { + showFilterBottomSheet(LibraryEvent.OnShowFilterBottomSheet) + } NavigatorIconButton() } ), @@ -188,11 +246,18 @@ fun LibraryTopBar( ), ), customContent = { - LibraryTabs( - categories = categories, - pagerState = pagerState, - itemCountBackgroundColor = animatedItemCountBackgroundColor.value - ) + AnimatedVisibility( + visible = showCategoryTabs, + enter = expandVertically(), + exit = shrinkVertically() + ) { + LibraryTabs( + categoriesWithBookCount = categoriesWithBookCount.value, + pagerState = pagerState, + itemCountBackgroundColor = animatedItemCountBackgroundColor.value, + showBookCount = showBookCount + ) + } } ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt index acd8baf0..8ad91d9e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsContent.kt @@ -19,6 +19,7 @@ fun SettingsContent( navigateToGeneralSettings: () -> Unit, navigateToAppearanceSettings: () -> Unit, navigateToReaderSettings: () -> Unit, + navigateToLibrarySettings: () -> Unit, navigateToBrowseSettings: () -> Unit, navigateBack: () -> Unit ) { @@ -28,6 +29,7 @@ fun SettingsContent( navigateToGeneralSettings = navigateToGeneralSettings, navigateToAppearanceSettings = navigateToAppearanceSettings, navigateToReaderSettings = navigateToReaderSettings, + navigateToLibrarySettings = navigateToLibrarySettings, navigateToBrowseSettings = navigateToBrowseSettings, navigateBack = navigateBack ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt index c4b32634..41829017 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsLayout.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.DisplaySettings import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.LocalLibrary @@ -29,6 +30,7 @@ fun SettingsLayout( navigateToGeneralSettings: () -> Unit, navigateToAppearanceSettings: () -> Unit, navigateToReaderSettings: () -> Unit, + navigateToLibrarySettings: () -> Unit, navigateToBrowseSettings: () -> Unit ) { LazyColumnWithScrollbar( @@ -74,6 +76,17 @@ fun SettingsLayout( item { SettingsLayoutItem( index = 3, + icon = Icons.Outlined.CollectionsBookmark, + title = stringResource(id = R.string.library_settings), + description = stringResource(id = R.string.library_settings_desc) + ) { + navigateToLibrarySettings() + } + } + + item { + SettingsLayoutItem( + index = 4, icon = Icons.Outlined.Explore, title = stringResource(id = R.string.browse_settings), description = stringResource(id = R.string.browse_settings_desc) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt index 979ba638..be3e798d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/SettingsScaffold.kt @@ -27,6 +27,7 @@ fun SettingsScaffold( navigateToGeneralSettings: () -> Unit, navigateToAppearanceSettings: () -> Unit, navigateToReaderSettings: () -> Unit, + navigateToLibrarySettings: () -> Unit, navigateToBrowseSettings: () -> Unit, navigateBack: () -> Unit ) { @@ -49,6 +50,7 @@ fun SettingsScaffold( navigateToGeneralSettings = navigateToGeneralSettings, navigateToAppearanceSettings = navigateToAppearanceSettings, navigateToReaderSettings = navigateToReaderSettings, + navigateToLibrarySettings = navigateToLibrarySettings, navigateToBrowseSettings = navigateToBrowseSettings ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt index 9b268724..ab3bd0cb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt @@ -10,15 +10,11 @@ package ua.acclorite.book_story.presentation.settings.browse import androidx.compose.foundation.lazy.LazyListScope import ua.acclorite.book_story.presentation.settings.browse.display.BrowseDisplaySubcategory -import ua.acclorite.book_story.presentation.settings.browse.filter.BrowseFilterSubcategory import ua.acclorite.book_story.presentation.settings.browse.scan.BrowseScanSubcategory -import ua.acclorite.book_story.presentation.settings.browse.sort.BrowseSortSubcategory fun LazyListScope.BrowseSettingsCategory() { BrowseScanSubcategory() - BrowseDisplaySubcategory() - BrowseFilterSubcategory() - BrowseSortSubcategory( + BrowseDisplaySubcategory( showDivider = false ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/display/components/BrowseGridSizeOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/display/components/BrowseGridSizeOption.kt index e76e3682..7c01904d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/display/components/BrowseGridSizeOption.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/display/components/BrowseGridSizeOption.kt @@ -25,12 +25,12 @@ fun BrowseGridSizeOption() { ExpandingTransition(visible = state.value.browseLayout == BrowseLayout.GRID) { SliderWithTitle( value = state.value.browseGridSize - to " ${stringResource(R.string.browse_grid_size_per_row)}", - valuePlaceholder = stringResource(id = R.string.browse_grid_size_auto), + to " ${stringResource(R.string.grid_size_per_row)}", + valuePlaceholder = stringResource(id = R.string.grid_size_auto), showPlaceholder = state.value.browseAutoGridSize, fromValue = 0, toValue = 10, - title = stringResource(id = R.string.browse_grid_size_option), + title = stringResource(id = R.string.grid_size_option), onValueChange = { mainModel.onEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0)) mainModel.onEvent( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt index fc8ae2db..4ad99871 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/BrowseSortSubcategory.kt @@ -19,7 +19,7 @@ import ua.acclorite.book_story.presentation.settings.components.SettingsSubcateg fun LazyListScope.BrowseSortSubcategory( titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, - title: @Composable () -> String = { stringResource(id = R.string.sort_browse_settings) }, + title: @Composable () -> String = { stringResource(id = R.string.sort_settings) }, showTitle: Boolean = true, showDivider: Boolean = true ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt index a2b2e0f9..eb89e995 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/sort/components/BrowseSortOption.kt @@ -90,10 +90,10 @@ private fun BrowseSortOptionItem( StyledText( text = stringResource( when (item) { - BrowseSortOrder.NAME -> R.string.browse_sort_order_name - BrowseSortOrder.FILE_FORMAT -> R.string.browse_sort_order_file_format - BrowseSortOrder.LAST_MODIFIED -> R.string.browse_sort_order_last_modified - BrowseSortOrder.FILE_SIZE -> R.string.browse_sort_order_file_size + BrowseSortOrder.NAME -> R.string.sort_order_name + BrowseSortOrder.FILE_FORMAT -> R.string.sort_order_file_format + BrowseSortOrder.LAST_MODIFIED -> R.string.sort_order_last_modified + BrowseSortOrder.FILE_SIZE -> R.string.sort_order_file_size } ), style = MaterialTheme.typography.bodyLarge, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsCategory.kt new file mode 100644 index 00000000..1aca1149 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsCategory.kt @@ -0,0 +1,24 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library + +import androidx.compose.foundation.lazy.LazyListScope +import ua.acclorite.book_story.presentation.settings.library.categories.LibraryCategoriesSubcategory +import ua.acclorite.book_story.presentation.settings.library.display.LibraryDisplaySubcategory +import ua.acclorite.book_story.presentation.settings.library.sort.LibrarySortSubcategory +import ua.acclorite.book_story.presentation.settings.library.tabs.LibraryTabsSubcategory + +fun LazyListScope.LibrarySettingsCategory() { + LibraryCategoriesSubcategory() + LibraryDisplaySubcategory() + LibraryTabsSubcategory() + LibrarySortSubcategory( + showDivider = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsContent.kt new file mode 100644 index 00000000..7ad2fd53 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsContent.kt @@ -0,0 +1,26 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LibrarySettingsContent( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LibrarySettingsScaffold( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsLayout.kt new file mode 100644 index 00000000..d23b072e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsLayout.kt @@ -0,0 +1,30 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar + +@Composable +fun LibrarySettingsLayout( + listState: LazyListState, + paddingValues: PaddingValues +) { + LazyColumnWithScrollbar( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + LibrarySettingsCategory() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsScaffold.kt new file mode 100644 index 00000000..0a5c7a90 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsScaffold.kt @@ -0,0 +1,47 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LibrarySettingsScaffold( + listState: LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LibrarySettingsTopBar( + scrollBehavior = scrollBehavior, + navigateBack = navigateBack + ) + } + ) { paddingValues -> + LibrarySettingsLayout( + listState = listState, + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsTopBar.kt new file mode 100644 index 00000000..2719ac4e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/LibrarySettingsTopBar.kt @@ -0,0 +1,41 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.navigator.NavigatorBackIconButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LibrarySettingsTopBar( + scrollBehavior: TopAppBarScrollBehavior, + navigateBack: () -> Unit +) { + LargeTopAppBar( + title = { + StyledText(stringResource(id = R.string.library_settings)) + }, + navigationIcon = { + NavigatorBackIconButton( + navigateBack = navigateBack + ) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/LibraryCategoriesSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/LibraryCategoriesSubcategory.kt new file mode 100644 index 00000000..305d8fe6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/LibraryCategoriesSubcategory.kt @@ -0,0 +1,41 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library.categories + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.library.categories.components.LibraryCategoriesOption +import ua.acclorite.book_story.presentation.settings.library.categories.components.LibraryCategoriesOptionNote + +fun LazyListScope.LibraryCategoriesSubcategory( + titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, + title: @Composable () -> String = { stringResource(id = R.string.categories_settings) }, + showTitle: Boolean = true, + showDivider: Boolean = true +) { + SettingsSubcategory( + titleColor = titleColor, + title = title, + showTitle = showTitle, + showDivider = showDivider + ) { + item { + LibraryCategoriesOption() + } + + item { + LibraryCategoriesOptionNote() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOption.kt new file mode 100644 index 00000000..9d1b2d4e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOption.kt @@ -0,0 +1,368 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.categories.components + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Label +import androidx.compose.material.icons.automirrored.outlined.LabelOff +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.EditNote +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import sh.calvin.reorderable.ReorderableColumn +import sh.calvin.reorderable.ReorderableScope +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.presentation.core.components.common.IconButton +import ua.acclorite.book_story.presentation.core.components.common.StyledText +import ua.acclorite.book_story.presentation.core.components.dialog.Dialog +import ua.acclorite.book_story.presentation.core.components.dialog.DialogWithTextField +import ua.acclorite.book_story.presentation.core.util.noRippleClickable +import ua.acclorite.book_story.ui.library.LibraryScreen +import ua.acclorite.book_story.ui.settings.SettingsEvent +import ua.acclorite.book_story.ui.settings.SettingsModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition +import ua.acclorite.book_story.ui.theme.dynamicListItemColor + +private const val CREATE_DIALOG = "create_dialog" +private const val EDIT_DIALOG = "edit_dialog" +private const val REMOVE_DIALOG = "remove_dialog" + +@Composable +fun LibraryCategoriesOption() { + val settingsModel = hiltViewModel() + val state = settingsModel.state.collectAsStateWithLifecycle() + + val dialog = remember { + mutableStateOf(null) + } + val selectedCategory = remember { + mutableStateOf(null) + } + + when (dialog.value) { + CREATE_DIALOG -> { + LibraryCategoriesCreateDialog( + onDismiss = { dialog.value = null }, + onAction = { title -> + settingsModel.onEvent( + SettingsEvent.OnCreateCategory( + title = title + ) + ) + } + ) + } + + EDIT_DIALOG -> { + LibraryCategoriesEditDialog( + initialValue = selectedCategory.value?.title ?: "", + onDismiss = { dialog.value = null }, + onAction = { title -> + settingsModel.onEvent( + SettingsEvent.OnUpdateCategoryTitle( + id = selectedCategory.value?.id ?: -1, + title = title + ) + ) + } + ) + } + + REMOVE_DIALOG -> { + LibraryCategoriesRemoveDialog( + onDismiss = { dialog.value = null }, + onAction = { + if (selectedCategory.value != null) { + LibraryScreen.scrollToPageCompositionChannel.trySend(0) + settingsModel.onEvent( + SettingsEvent.OnRemoveCategory(selectedCategory.value!!) + ) + } + dialog.value = null + } + ) + } + } + + ReorderableColumn( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + list = state.value.categories, + onSettle = { from, to -> + LibraryScreen.scrollToPageCompositionChannel.trySend(0) + settingsModel.onEvent( + SettingsEvent.OnUpdateCategoryOrder( + state.value.categories.toMutableList().apply { + add(to, removeAt(from)) + } + ) + ) + } + ) { _, category, _ -> + key(category.id) { + LibraryCategoryItem( + category = category, + edit = { + selectedCategory.value = category + dialog.value = EDIT_DIALOG + }, + remove = { + selectedCategory.value = category + dialog.value = REMOVE_DIALOG + } + ) + } + } + + LibraryCategoriesAction { + dialog.value = CREATE_DIALOG + } +} + +@Composable +private fun LibraryCategoriesEditDialog( + initialValue: String, + onDismiss: () -> Unit, + onAction: (title: String) -> Unit +) { + DialogWithTextField( + initialValue = initialValue, + lengthLimit = 200, + onDismiss = { + onDismiss() + }, + onAction = { + if (it.isBlank()) return@DialogWithTextField + onAction(it.replace("\n", "").trim()) + } + ) +} + +@Composable +private fun LibraryCategoriesRemoveDialog( + onDismiss: () -> Unit, + onAction: () -> Unit +) { + Dialog( + title = stringResource(id = R.string.remove_category), + icon = Icons.AutoMirrored.Outlined.LabelOff, + description = stringResource(id = R.string.remove_category_description), + actionEnabled = true, + onDismiss = { + onDismiss() + }, + onAction = { + onAction() + }, + withContent = false + ) +} + +@Composable +private fun LibraryCategoriesCreateDialog( + onDismiss: () -> Unit, + onAction: (title: String) -> Unit +) { + val title = remember { + mutableStateOf("") + } + val error = remember { + mutableStateOf(false) + } + + val focusRequester = remember { FocusRequester() } + val focused = remember { mutableStateOf(false) } + + Dialog( + title = stringResource(id = R.string.create_category), + description = null, + onDismiss = { onDismiss() }, + actionEnabled = true, + disableOnClick = false, + onAction = { + if (title.value.isBlank()) { + error.value = true + return@Dialog + } + + onAction(title.value.replace("\n", "").trim()) + onDismiss() + }, + withContent = true, + items = { + item { + OutlinedTextField( + value = title.value, + onValueChange = { + error.value = false + if (it.length <= 200) { + title.value = it + } + }, + keyboardOptions = KeyboardOptions(KeyboardCapitalization.Sentences), + isError = error.value, + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth() + .focusRequester(focusRequester) + .onGloballyPositioned { + if (!focused.value) { + focusRequester.requestFocus() + focused.value = true + } + }, + colors = TextFieldDefaults.colors( + errorContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent + ), + label = { + StyledText( + text = stringResource( + id = R.string.library_categories_create_category_placeholder + ), + maxLines = 1 + ) + }, + supportingText = { + ExpandingTransition(error.value) { + StyledText( + text = stringResource(id = R.string.error_field_empty), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.error + ) + ) + } + } + ) + } + } + ) +} + +@Composable +private fun LibraryCategoriesAction( + create: () -> Unit +) { + Row( + modifier = Modifier + .padding(18.dp) + .noRippleClickable { + create() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.Add, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + StyledText( + text = stringResource(id = R.string.create_category), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.secondary + ) + ) + } +} + +@Composable +private fun ReorderableScope.LibraryCategoryItem( + category: Category, + edit: () -> Unit, + remove: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 18.dp, + vertical = 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Label, + contentDescription = stringResource(id = R.string.drag_content_desc), + modifier = Modifier + .clip(CircleShape) + .draggableHandle() + .background(MaterialTheme.colorScheme.dynamicListItemColor(category.order)) + .padding(11.dp) + .size(22.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.width(18.dp)) + + StyledText( + text = category.title, + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(18.dp)) + + IconButton( + modifier = Modifier.size(24.dp), + icon = Icons.Outlined.EditNote, + contentDescription = R.string.edit_content_desc, + disableOnClick = false, + color = MaterialTheme.colorScheme.onSurface + ) { + edit() + } + + IconButton( + modifier = Modifier.size(24.dp), + icon = Icons.Outlined.Clear, + contentDescription = R.string.remove_content_desc, + disableOnClick = false, + color = MaterialTheme.colorScheme.onSurface + ) { + remove() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOptionNote.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOptionNote.kt new file mode 100644 index 00000000..d0bf5689 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/categories/components/LibraryCategoriesOptionNote.kt @@ -0,0 +1,19 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.categories.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryNote + +@Composable +fun LibraryCategoriesOptionNote() { + SettingsSubcategoryNote( + text = stringResource(id = R.string.library_categories_option_note) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/LibraryDisplaySubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/LibraryDisplaySubcategory.kt new file mode 100644 index 00000000..10c7e103 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/LibraryDisplaySubcategory.kt @@ -0,0 +1,56 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library.display + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.library.display.components.LibraryGridSizeOption +import ua.acclorite.book_story.presentation.settings.library.display.components.LibraryLayoutOption +import ua.acclorite.book_story.presentation.settings.library.display.components.LibraryReadButtonOption +import ua.acclorite.book_story.presentation.settings.library.display.components.LibraryShowProgressOption +import ua.acclorite.book_story.presentation.settings.library.display.components.LibraryTitlePositionOption + +fun LazyListScope.LibraryDisplaySubcategory( + titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, + title: @Composable () -> String = { stringResource(id = R.string.display_settings) }, + showTitle: Boolean = true, + showDivider: Boolean = true +) { + SettingsSubcategory( + titleColor = titleColor, + title = title, + showTitle = showTitle, + showDivider = showDivider + ) { + item { + LibraryLayoutOption() + } + + item { + LibraryGridSizeOption() + } + + item { + LibraryTitlePositionOption() + } + + item { + LibraryReadButtonOption() + } + + item { + LibraryShowProgressOption() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryGridSizeOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryGridSizeOption.kt new file mode 100644 index 00000000..e7843c7e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryGridSizeOption.kt @@ -0,0 +1,42 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.display.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition + +@Composable +fun LibraryGridSizeOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + ExpandingTransition(visible = state.value.libraryLayout == LibraryLayout.GRID) { + SliderWithTitle( + value = state.value.libraryGridSize + to " ${stringResource(R.string.grid_size_per_row)}", + valuePlaceholder = stringResource(id = R.string.grid_size_auto), + showPlaceholder = state.value.libraryAutoGridSize, + fromValue = 0, + toValue = 10, + title = stringResource(id = R.string.grid_size_option), + onValueChange = { + mainModel.onEvent(MainEvent.OnChangeLibraryAutoGridSize(it == 0)) + mainModel.onEvent( + MainEvent.OnChangeLibraryGridSize(it) + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryLayoutOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryLayoutOption.kt new file mode 100644 index 00000000..9cd8cb82 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryLayoutOption.kt @@ -0,0 +1,46 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.display.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.presentation.core.components.settings.SegmentedButtonWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryLayoutOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SegmentedButtonWithTitle( + title = stringResource(id = R.string.layout_option), + buttons = LibraryLayout.entries.map { + ButtonItem( + it.toString(), + when (it) { + LibraryLayout.LIST -> stringResource(id = R.string.layout_list) + LibraryLayout.GRID -> stringResource(id = R.string.layout_grid) + }, + MaterialTheme.typography.labelLarge, + it == state.value.libraryLayout + ) + } + ) { + mainModel.onEvent( + MainEvent.OnChangeLibraryLayout( + it.id + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryReadButtonOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryReadButtonOption.kt new file mode 100644 index 00000000..48b75fc3 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryReadButtonOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.display.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryReadButtonOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryReadButton, + title = stringResource(id = R.string.read_button_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryReadButton( + !state.value.libraryReadButton + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryShowProgressOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryShowProgressOption.kt new file mode 100644 index 00000000..766e1b7d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryShowProgressOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.display.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryShowProgressOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryShowProgress, + title = stringResource(id = R.string.show_progress_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryShowProgress( + !state.value.libraryShowProgress + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryTitlePositionOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryTitlePositionOption.kt new file mode 100644 index 00000000..5728d1e6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/display/components/LibraryTitlePositionOption.kt @@ -0,0 +1,59 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.display.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition +import ua.acclorite.book_story.domain.ui.ButtonItem +import ua.acclorite.book_story.presentation.core.components.settings.ChipsWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel +import ua.acclorite.book_story.ui.theme.ExpandingTransition + +@Composable +fun LibraryTitlePositionOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + ExpandingTransition(visible = state.value.libraryLayout == LibraryLayout.GRID) { + ChipsWithTitle( + title = stringResource(id = R.string.title_position_option), + chips = LibraryTitlePosition.entries.map { + ButtonItem( + it.toString(), + when (it) { + LibraryTitlePosition.OFF -> { + stringResource(id = R.string.library_title_position_off) + } + + LibraryTitlePosition.BELOW -> { + stringResource(id = R.string.library_title_position_below) + } + + LibraryTitlePosition.INSIDE -> { + stringResource(id = R.string.library_title_position_inside) + } + }, + MaterialTheme.typography.labelLarge, + it == state.value.libraryTitlePosition + ) + } + ) { + mainModel.onEvent( + MainEvent.OnChangeLibraryTitlePosition( + it.id + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/LibrarySortSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/LibrarySortSubcategory.kt new file mode 100644 index 00000000..74078ab5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/LibrarySortSubcategory.kt @@ -0,0 +1,36 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library.sort + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.library.sort.components.LibraryPerCategorySortOption + +fun LazyListScope.LibrarySortSubcategory( + titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, + title: @Composable () -> String = { stringResource(id = R.string.sort_settings) }, + showTitle: Boolean = true, + showDivider: Boolean = true +) { + SettingsSubcategory( + titleColor = titleColor, + title = title, + showTitle = showTitle, + showDivider = showDivider + ) { + item { + LibraryPerCategorySortOption() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibraryPerCategorySortOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibraryPerCategorySortOption.kt new file mode 100644 index 00000000..93658b11 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibraryPerCategorySortOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.sort.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryPerCategorySortOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryPerCategorySort, + title = stringResource(id = R.string.per_category_sort_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryPerCategorySort( + !state.value.libraryPerCategorySort + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibrarySortOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibrarySortOption.kt new file mode 100644 index 00000000..7938b98b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/sort/components/LibrarySortOption.kt @@ -0,0 +1,95 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library.sort.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.presentation.core.components.common.StyledText + +fun LazyListScope.LibrarySortOption( + sortOrder: LibrarySortOrder, + sortOrderDescending: Boolean, + onChange: (LibrarySortOrder, Boolean) -> Unit +) { + items(LibrarySortOrder.entries, key = { it.name }) { + LibrarySortOptionItem( + item = it, + isSelected = sortOrder == it, + isDescending = sortOrderDescending + ) { + if (sortOrder == it) { + onChange(sortOrder, !sortOrderDescending) + } else { + onChange(it, true) + } + } + } +} + +@Composable +private fun LibrarySortOptionItem( + item: LibrarySortOrder, + isSelected: Boolean, + isDescending: Boolean, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + } + .padding(horizontal = 24.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (isDescending) Icons.Default.ArrowDownward + else Icons.Default.ArrowUpward, + contentDescription = stringResource(id = R.string.sort_order_content_desc), + modifier = Modifier + .size(28.dp), + tint = if (isSelected) MaterialTheme.colorScheme.secondary + else Color.Transparent + ) + Spacer(modifier = Modifier.width(24.dp)) + + StyledText( + text = stringResource( + when (item) { + LibrarySortOrder.NAME -> R.string.sort_order_name + LibrarySortOrder.LAST_READ -> R.string.sort_order_last_read + LibrarySortOrder.PROGRESS -> R.string.sort_order_progress + LibrarySortOrder.AUTHOR -> R.string.sort_order_author + } + ), + style = MaterialTheme.typography.bodyLarge, + maxLines = 1 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/LibraryTabsSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/LibraryTabsSubcategory.kt new file mode 100644 index 00000000..af7315e5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/LibraryTabsSubcategory.kt @@ -0,0 +1,46 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +@file:Suppress("FunctionName") + +package ua.acclorite.book_story.presentation.settings.library.tabs + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory +import ua.acclorite.book_story.presentation.settings.library.tabs.components.LibraryAlwaysShowDefaultTabOption +import ua.acclorite.book_story.presentation.settings.library.tabs.components.LibraryShowBookCountOption +import ua.acclorite.book_story.presentation.settings.library.tabs.components.LibraryShowCategoryTabsOption + +fun LazyListScope.LibraryTabsSubcategory( + titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, + title: @Composable () -> String = { stringResource(id = R.string.tabs_settings) }, + showTitle: Boolean = true, + showDivider: Boolean = true +) { + SettingsSubcategory( + titleColor = titleColor, + title = title, + showTitle = showTitle, + showDivider = showDivider + ) { + item { + LibraryShowCategoryTabsOption() + } + + item { + LibraryAlwaysShowDefaultTabOption() + } + + item { + LibraryShowBookCountOption() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryAlwaysShowDefaultTabOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryAlwaysShowDefaultTabOption.kt new file mode 100644 index 00000000..2c390962 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryAlwaysShowDefaultTabOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.tabs.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryAlwaysShowDefaultTabOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryAlwaysShowDefaultTab, + title = stringResource(id = R.string.always_show_default_tab_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryAlwaysShowDefaultTab( + !state.value.libraryAlwaysShowDefaultTab + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowBookCountOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowBookCountOption.kt new file mode 100644 index 00000000..598b11fc --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowBookCountOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.tabs.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryShowBookCountOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryShowBookCount, + title = stringResource(id = R.string.show_book_count_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryShowBookCount( + !state.value.libraryShowBookCount + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowCategoryTabsOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowCategoryTabsOption.kt new file mode 100644 index 00000000..3568df2e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/library/tabs/components/LibraryShowCategoryTabsOption.kt @@ -0,0 +1,34 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.presentation.settings.library.tabs.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.core.components.settings.SwitchWithTitle +import ua.acclorite.book_story.ui.main.MainEvent +import ua.acclorite.book_story.ui.main.MainModel + +@Composable +fun LibraryShowCategoryTabsOption() { + val mainModel = hiltViewModel() + val state = mainModel.state.collectAsStateWithLifecycle() + + SwitchWithTitle( + selected = state.value.libraryShowCategoryTabs, + title = stringResource(id = R.string.show_category_tabs_option), + onClick = { + mainModel.onEvent( + MainEvent.OnChangeLibraryShowCategoryTabs( + !state.value.libraryShowCategoryTabs + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt index 74257096..3ba33ac1 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoEvent.kt @@ -74,9 +74,8 @@ sealed class BookInfoEvent { data object OnShowMoveDialog : BookInfoEvent() data class OnActionMoveDialog( - val category: Category, - val context: Context, - val navigateToLibrary: () -> Unit + val selectedCategories: List, + val context: Context ) : BookInfoEvent() data object OnDismissDialog : BookInfoEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt index 00a10ce6..10a391b9 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.library.category.Category import ua.acclorite.book_story.domain.use_case.book.CanResetCover import ua.acclorite.book_story.domain.use_case.book.DeleteBooks import ua.acclorite.book_story.domain.use_case.book.GetBookById @@ -357,34 +356,22 @@ class BookInfoModel @Inject constructor( launch { _state.update { it.copy( + book = it.book.copy( + categories = event.selectedCategories.map { it.id } + ), dialog = null, bottomSheet = null ) } - - _state.update { - it.copy( - book = it.book.copy( - category = event.category - ) - ) - } updateBook.execute(_state.value.book) LibraryScreen.refreshListChannel.trySend(0) - LibraryScreen.scrollToPageCompositionChannel.trySend( - Category.entries.dropLastWhile { - it != event.category - }.size - 1 - ) HistoryScreen.refreshListChannel.trySend(0) withContext(Dispatchers.Main) { event.context.getString(R.string.book_moved) .showToast(context = event.context) } - - event.navigateToLibrary() } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt index 1b055579..234b6460 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/book_info/BookInfoScreen.kt @@ -23,8 +23,9 @@ import ua.acclorite.book_story.domain.navigator.Screen import ua.acclorite.book_story.presentation.book_info.BookInfoContent import ua.acclorite.book_story.presentation.navigator.LocalNavigator import ua.acclorite.book_story.ui.history.HistoryScreen -import ua.acclorite.book_story.ui.library.LibraryScreen import ua.acclorite.book_story.ui.reader.ReaderScreen +import ua.acclorite.book_story.ui.settings.LibrarySettingsScreen +import ua.acclorite.book_story.ui.settings.SettingsModel @Parcelize data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { @@ -47,7 +48,9 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { override fun Content() { val navigator = LocalNavigator.current val screenModel = hiltViewModel() + val settingsModel = hiltViewModel() + val settingsState = settingsModel.state.collectAsStateWithLifecycle() val state = screenModel.state.collectAsStateWithLifecycle() val listState = rememberLazyListState() @@ -72,6 +75,7 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { if (state.value.book.id == bookId) { BookInfoContent( book = state.value.book, + categories = settingsState.value.categories, bottomSheet = state.value.bottomSheet, dialog = state.value.dialog, listState = listState, @@ -102,12 +106,8 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable { navigator.push(ReaderScreen(state.value.book.id)) } }, - navigateToLibrary = { - navigator.push( - LibraryScreen, - popping = true, - saveInBackStack = false - ) + navigateToLibrarySettings = { + navigator.push(LibrarySettingsScreen) }, navigateBack = { navigator.pop() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt index a65bcd42..1938e0a4 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt @@ -29,6 +29,7 @@ import ua.acclorite.book_story.domain.library.book.SelectableNullableBook import ua.acclorite.book_story.domain.use_case.book.InsertBook import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile import ua.acclorite.book_story.domain.use_case.file_system.GetFiles +import ua.acclorite.book_story.presentation.core.util.compareByWithOrder import ua.acclorite.book_story.presentation.core.util.showToast import ua.acclorite.book_story.ui.library.LibraryScreen import javax.inject.Inject @@ -419,16 +420,6 @@ class BrowseModel @Inject constructor( includedFilterItems: List, sortOrder: BrowseSortOrder ): List { - fun compareByWithOrder( - selector: (T) -> Comparable<*>? - ): Comparator { - return if (sortOrderDescending) { - compareByDescending(selector) - } else { - compareBy(selector) - } - } - fun List.filterFiles(): List { if (includedFilterItems.isEmpty()) { return this @@ -446,7 +437,7 @@ class BrowseModel @Inject constructor( return files .filterFiles() .sortedWith( - compareByWithOrder { + compareByWithOrder(sortOrderDescending) { when (sortOrder) { BrowseSortOrder.NAME -> { it.data.name.trim() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt index 9216d5ec..c3b4287b 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryEvent.kt @@ -10,7 +10,6 @@ import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.library.category.Category -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks @Immutable sealed class LibraryEvent { @@ -43,8 +42,7 @@ sealed class LibraryEvent { data object OnShowMoveDialog : LibraryEvent() data class OnActionMoveDialog( - val selectedCategory: Category, - val categories: List, + val selectedCategories: List, val context: Context ) : LibraryEvent() @@ -55,4 +53,8 @@ sealed class LibraryEvent { ) : LibraryEvent() data object OnDismissDialog : LibraryEvent() + + data object OnShowFilterBottomSheet : LibraryEvent() + + data object OnDismissBottomSheet : LibraryEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt index 0f5530ad..b2a83041 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryModel.kt @@ -210,7 +210,7 @@ class LibraryModel @Inject constructor( if (!book.selected) return@forEach moveBooks.execute( book.data.copy( - category = event.selectedCategory + categories = event.selectedCategories.map { it.id } ) ) } @@ -221,7 +221,7 @@ class LibraryModel @Inject constructor( if (!book.selected) return@map book book.copy( data = book.data.copy( - category = event.selectedCategory + categories = event.selectedCategories.map { it.id } ), selected = false ) @@ -232,11 +232,6 @@ class LibraryModel @Inject constructor( } HistoryScreen.refreshListChannel.trySend(0) - LibraryScreen.scrollToPageCompositionChannel.trySend( - event.categories.dropLastWhile { - it.category != event.selectedCategory - }.lastIndex - ) withContext(Dispatchers.Main) { event.context @@ -293,6 +288,26 @@ class LibraryModel @Inject constructor( } } } + + is LibraryEvent.OnShowFilterBottomSheet -> { + viewModelScope.launch { + _state.update { + it.copy( + bottomSheet = LibraryScreen.FILTER_BOTTOM_SHEET + ) + } + } + } + + is LibraryEvent.OnDismissBottomSheet -> { + viewModelScope.launch { + _state.update { + it.copy( + bottomSheet = null + ) + } + } + } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt index fae73161..8c2d786c 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryScreen.kt @@ -23,11 +23,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.library.category.Category -import ua.acclorite.book_story.domain.library.category.CategoryWithBooks import ua.acclorite.book_story.domain.navigator.Screen -import ua.acclorite.book_story.domain.ui.UIText import ua.acclorite.book_story.presentation.library.LibraryContent import ua.acclorite.book_story.presentation.navigator.LocalNavigator import ua.acclorite.book_story.ui.book_info.BookInfoScreen @@ -35,6 +31,8 @@ import ua.acclorite.book_story.ui.browse.BrowseScreen import ua.acclorite.book_story.ui.history.HistoryScreen import ua.acclorite.book_story.ui.main.MainModel import ua.acclorite.book_story.ui.reader.ReaderScreen +import ua.acclorite.book_story.ui.settings.LibrarySettingsScreen +import ua.acclorite.book_story.ui.settings.SettingsModel @Parcelize object LibraryScreen : Screen, Parcelable { @@ -45,6 +43,9 @@ object LibraryScreen : Screen, Parcelable { @IgnoredOnParcel const val DELETE_DIALOG = "delete_dialog" + @IgnoredOnParcel + const val FILTER_BOTTOM_SHEET = "filter_bottom_sheet" + @IgnoredOnParcel val refreshListChannel: Channel = Channel(Channel.CONFLATED) @@ -61,34 +62,19 @@ object LibraryScreen : Screen, Parcelable { val screenModel = hiltViewModel() val mainModel = hiltViewModel() + val settingsModel = hiltViewModel() val state = screenModel.state.collectAsStateWithLifecycle() val mainState = mainModel.state.collectAsStateWithLifecycle() + val settingsState = settingsModel.state.collectAsStateWithLifecycle() - val categories = remember(state.value.books) { + val showDefaultCategory = remember(state.value.books, settingsState.value.categories) { derivedStateOf { - listOf( - CategoryWithBooks( - category = Category.READING, - title = UIText.StringResource(R.string.reading_tab), - books = state.value.books.filter { it.data.category == Category.READING } - ), - CategoryWithBooks( - category = Category.ALREADY_READ, - title = UIText.StringResource(R.string.already_read_tab), - books = state.value.books.filter { it.data.category == Category.ALREADY_READ } - ), - CategoryWithBooks( - category = Category.PLANNING, - title = UIText.StringResource(R.string.planning_tab), - books = state.value.books.filter { it.data.category == Category.PLANNING } - ), - CategoryWithBooks( - category = Category.DROPPED, - title = UIText.StringResource(R.string.dropped_tab), - books = state.value.books.filter { it.data.category == Category.DROPPED } - ) - ) + val categoryIds = settingsState.value.categories.map { it.id }.toSet() + state.value.books.any { book -> + book.data.categories.none { category -> category in categoryIds } + } || settingsState.value.categories.isEmpty() + || mainState.value.libraryAlwaysShowDefaultTab } } @@ -107,12 +93,14 @@ object LibraryScreen : Screen, Parcelable { val pagerState = rememberPagerState( initialPage = initialPage - ) { categories.value.count() } + ) { + settingsState.value.categories.count().plus(if (showDefaultCategory.value) 1 else 0) + } DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } } LaunchedEffect(Unit) { scrollToPageCompositionChannel.receiveAsFlow().collectLatest { - pagerState.animateScrollToPage(it) + pagerState.scrollToPage(it) } } @@ -120,6 +108,11 @@ object LibraryScreen : Screen, Parcelable { books = state.value.books, selectedItemsCount = state.value.selectedItemsCount, hasSelectedItems = state.value.hasSelectedItems, + titlePosition = mainState.value.libraryTitlePosition, + readButton = mainState.value.libraryReadButton, + showProgress = mainState.value.libraryShowProgress, + showCategoryTabs = mainState.value.libraryShowCategoryTabs, + showBookCount = mainState.value.libraryShowBookCount, showSearch = state.value.showSearch, searchQuery = state.value.searchQuery, bookCount = state.value.books.count(), @@ -128,9 +121,21 @@ object LibraryScreen : Screen, Parcelable { isLoading = state.value.isLoading, isRefreshing = state.value.isRefreshing, doublePressExit = mainState.value.doublePressExit, - categories = categories.value, + layout = mainState.value.libraryLayout, + gridSize = mainState.value.libraryGridSize, + autoGridSize = mainState.value.libraryAutoGridSize, + categories = settingsState.value.categories, + showDefaultCategory = showDefaultCategory.value, + categoriesSort = settingsState.value.categoriesSort, + sortOrder = mainState.value.librarySortOrder, + sortOrderDescending = mainState.value.librarySortOrderDescending, + perCategorySort = mainState.value.libraryPerCategorySort, refreshState = refreshState, dialog = state.value.dialog, + bottomSheet = state.value.bottomSheet, + updateCategorySort = settingsModel::onEvent, + changeLibrarySortOrder = mainModel::onEvent, + changeLibrarySortOrderDescending = mainModel::onEvent, selectBook = screenModel::onEvent, searchVisibility = screenModel::onEvent, requestFocus = screenModel::onEvent, @@ -141,6 +146,8 @@ object LibraryScreen : Screen, Parcelable { actionMoveDialog = screenModel::onEvent, actionDeleteDialog = screenModel::onEvent, showDeleteDialog = screenModel::onEvent, + showFilterBottomSheet = screenModel::onEvent, + dismissBottomSheet = screenModel::onEvent, dismissDialog = screenModel::onEvent, navigateToBrowse = { navigator.push(BrowseScreen) @@ -151,6 +158,9 @@ object LibraryScreen : Screen, Parcelable { }, navigateToBookInfo = { navigator.push(BookInfoScreen(bookId = it)) + }, + navigateToLibrarySettings = { + navigator.push(LibrarySettingsScreen) } ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt index d1972b76..2dca870e 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/library/LibraryState.kt @@ -8,6 +8,7 @@ package ua.acclorite.book_story.ui.library import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.library.book.SelectableBook +import ua.acclorite.book_story.domain.util.BottomSheet import ua.acclorite.book_story.domain.util.Dialog @Immutable @@ -24,5 +25,6 @@ data class LibraryState( val searchQuery: String = "", val hasFocused: Boolean = false, - val dialog: Dialog? = null + val dialog: Dialog? = null, + val bottomSheet: BottomSheet? = null ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt index 3c406ee5..25e50d07 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainEvent.kt @@ -67,4 +67,16 @@ sealed class MainEvent { data class OnChangeProgressCount(val value: String) : MainEvent() data class OnChangeHorizontalGestureAlphaAnim(val value: Boolean) : MainEvent() data class OnChangeHorizontalGesturePullAnim(val value: Boolean) : MainEvent() + data class OnChangeLibraryLayout(val value: String) : MainEvent() + data class OnChangeLibraryAutoGridSize(val value: Boolean) : MainEvent() + data class OnChangeLibraryGridSize(val value: Int) : MainEvent() + data class OnChangeLibraryReadButton(val value: Boolean) : MainEvent() + data class OnChangeLibraryShowProgress(val value: Boolean) : MainEvent() + data class OnChangeLibraryTitlePosition(val value: String) : MainEvent() + data class OnChangeLibraryShowBookCount(val value: Boolean) : MainEvent() + data class OnChangeLibraryShowCategoryTabs(val value: Boolean) : MainEvent() + data class OnChangeLibraryAlwaysShowDefaultTab(val value: Boolean) : MainEvent() + data class OnChangeLibrarySortOrder(val value: String) : MainEvent() + data class OnChangeLibrarySortOrderDescending(val value: Boolean) : MainEvent() + data class OnChangeLibraryPerCategorySort(val value: Boolean) : MainEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt index f6cbe396..1a49a900 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainModel.kt @@ -24,6 +24,9 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.domain.browse.display.toBrowseLayout import ua.acclorite.book_story.domain.browse.display.toBrowseSortOrder +import ua.acclorite.book_story.domain.library.display.toLibraryLayout +import ua.acclorite.book_story.domain.library.display.toLibrarySortOrder +import ua.acclorite.book_story.domain.library.display.toLibraryTitlePosition import ua.acclorite.book_story.domain.reader.toColorEffects import ua.acclorite.book_story.domain.reader.toFontThickness import ua.acclorite.book_story.domain.reader.toHorizontalGesture @@ -514,6 +517,102 @@ class MainModel @Inject constructor( it.copy(horizontalGesturePullAnim = this) } ) + + is MainEvent.OnChangeLibraryLayout -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_LAYOUT, + value = event.value, + updateState = { + it.copy(libraryLayout = this.toLibraryLayout()) + } + ) + + is MainEvent.OnChangeLibraryAutoGridSize -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_AUTO_GRID_SIZE, + value = event.value, + updateState = { + it.copy(libraryAutoGridSize = this) + } + ) + + is MainEvent.OnChangeLibraryGridSize -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_GRID_SIZE, + value = event.value, + updateState = { + it.copy(libraryGridSize = this) + } + ) + + is MainEvent.OnChangeLibraryReadButton -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_READ_BUTTON, + value = event.value, + updateState = { + it.copy(libraryReadButton = this) + } + ) + + is MainEvent.OnChangeLibraryShowProgress -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_SHOW_PROGRESS, + value = event.value, + updateState = { + it.copy(libraryShowProgress = this) + } + ) + + is MainEvent.OnChangeLibraryTitlePosition -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_TITLE_POSITION, + value = event.value, + updateState = { + it.copy(libraryTitlePosition = this.toLibraryTitlePosition()) + } + ) + + is MainEvent.OnChangeLibraryShowBookCount -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_SHOW_BOOK_COUNT, + value = event.value, + updateState = { + it.copy(libraryShowBookCount = this) + } + ) + + is MainEvent.OnChangeLibraryShowCategoryTabs -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_SHOW_CATEGORY_TABS, + value = event.value, + updateState = { + it.copy(libraryShowCategoryTabs = this) + } + ) + + is MainEvent.OnChangeLibraryAlwaysShowDefaultTab -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_ALWAYS_SHOW_DEFAULT_TAB, + value = event.value, + updateState = { + it.copy(libraryAlwaysShowDefaultTab = this) + } + ) + + is MainEvent.OnChangeLibrarySortOrder -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_SORT_ORDER, + value = event.value, + updateState = { + it.copy(librarySortOrder = this.toLibrarySortOrder()) + } + ) + + is MainEvent.OnChangeLibrarySortOrderDescending -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_SORT_ORDER_DESCENDING, + value = event.value, + updateState = { + it.copy(librarySortOrderDescending = this) + } + ) + + is MainEvent.OnChangeLibraryPerCategorySort -> handleDatastoreUpdate( + key = DataStoreConstants.LIBRARY_PER_CATEGORY_SORT, + value = event.value, + updateState = { + it.copy(libraryPerCategorySort = this) + } + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt index c7e2ec43..f268ac30 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainState.kt @@ -17,6 +17,12 @@ import ua.acclorite.book_story.domain.browse.display.BrowseLayout import ua.acclorite.book_story.domain.browse.display.BrowseSortOrder import ua.acclorite.book_story.domain.browse.display.toBrowseLayout import ua.acclorite.book_story.domain.browse.display.toBrowseSortOrder +import ua.acclorite.book_story.domain.library.display.LibraryLayout +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder +import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition +import ua.acclorite.book_story.domain.library.display.toLibraryLayout +import ua.acclorite.book_story.domain.library.display.toLibrarySortOrder +import ua.acclorite.book_story.domain.library.display.toLibraryTitlePosition import ua.acclorite.book_story.domain.reader.ReaderColorEffects import ua.acclorite.book_story.domain.reader.ReaderFontThickness import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture @@ -117,6 +123,20 @@ data class MainState( val progressBarFontSize: Int = provideDefaultValue { 8 }, val progressCount: ReaderProgressCount = provideDefaultValue { ReaderProgressCount.PERCENTAGE }, + // Library settings + val libraryLayout: LibraryLayout = provideDefaultValue { LibraryLayout.GRID }, + val libraryAutoGridSize: Boolean = provideDefaultValue { true }, + val libraryGridSize: Int = provideDefaultValue { 0 }, + val libraryTitlePosition: LibraryTitlePosition = provideDefaultValue { LibraryTitlePosition.BELOW }, + val libraryReadButton: Boolean = provideDefaultValue { true }, + val libraryShowProgress: Boolean = provideDefaultValue { true }, + val libraryShowBookCount: Boolean = provideDefaultValue { true }, + val libraryShowCategoryTabs: Boolean = provideDefaultValue { true }, + val libraryAlwaysShowDefaultTab: Boolean = provideDefaultValue { false }, + val librarySortOrder: LibrarySortOrder = provideDefaultValue { LibrarySortOrder.LAST_READ }, + val librarySortOrderDescending: Boolean = provideDefaultValue { true }, + val libraryPerCategorySort: Boolean = provideDefaultValue { false }, + // Browse Settings val browseLayout: BrowseLayout = provideDefaultValue { BrowseLayout.LIST }, val browseAutoGridSize: Boolean = provideDefaultValue { true }, @@ -374,6 +394,54 @@ data class MainState( horizontalGesturePullAnim = provideValue( HORIZONTAL_GESTURE_PULL_ANIM ) { horizontalGesturePullAnim }, + + libraryLayout = provideValue( + LIBRARY_LAYOUT, convert = { toLibraryLayout() } + ) { libraryLayout }, + + libraryAutoGridSize = provideValue( + LIBRARY_AUTO_GRID_SIZE + ) { libraryAutoGridSize }, + + libraryGridSize = provideValue( + LIBRARY_GRID_SIZE + ) { libraryGridSize }, + + libraryReadButton = provideValue( + LIBRARY_READ_BUTTON + ) { libraryReadButton }, + + libraryShowProgress = provideValue( + LIBRARY_SHOW_PROGRESS + ) { libraryShowProgress }, + + libraryTitlePosition = provideValue( + LIBRARY_TITLE_POSITION, convert = { toLibraryTitlePosition() } + ) { libraryTitlePosition }, + + libraryShowBookCount = provideValue( + LIBRARY_SHOW_BOOK_COUNT + ) { libraryShowBookCount }, + + libraryShowCategoryTabs = provideValue( + LIBRARY_SHOW_CATEGORY_TABS + ) { libraryShowCategoryTabs }, + + libraryAlwaysShowDefaultTab = provideValue( + LIBRARY_ALWAYS_SHOW_DEFAULT_TAB + ) { libraryAlwaysShowDefaultTab }, + + librarySortOrder = provideValue( + LIBRARY_SORT_ORDER, convert = { toLibrarySortOrder() } + ) { librarySortOrder }, + + librarySortOrderDescending = provideValue( + LIBRARY_SORT_ORDER_DESCENDING + ) { librarySortOrderDescending }, + + libraryPerCategorySort = provideValue( + LIBRARY_PER_CATEGORY_SORT + ) { libraryPerCategorySort }, ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/LibrarySettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/LibrarySettingsScreen.kt new file mode 100644 index 00000000..0806e96a --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/LibrarySettingsScreen.kt @@ -0,0 +1,36 @@ +/* + * Book's Story — free and open-source Material You eBook reader. + * Copyright (C) 2024-2025 Acclorite + * SPDX-License-Identifier: GPL-3.0-only + */ + +package ua.acclorite.book_story.ui.settings + +import android.os.Parcelable +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import kotlinx.parcelize.Parcelize +import ua.acclorite.book_story.domain.navigator.Screen +import ua.acclorite.book_story.presentation.core.components.top_bar.collapsibleTopAppBarScrollBehavior +import ua.acclorite.book_story.presentation.navigator.LocalNavigator +import ua.acclorite.book_story.presentation.settings.library.LibrarySettingsContent + +@Parcelize +object LibrarySettingsScreen : Screen, Parcelable { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior() + + LibrarySettingsContent( + listState = listState, + scrollBehavior = scrollBehavior, + navigateBack = { + navigator.pop() + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt index de004a05..672c071a 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt @@ -10,6 +10,8 @@ import android.content.Context import android.net.Uri import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.display.LibrarySortOrder import ua.acclorite.book_story.domain.util.ID @Immutable @@ -22,6 +24,29 @@ sealed class SettingsEvent { val uri: Uri ) : SettingsEvent() + data class OnCreateCategory( + val title: String + ) : SettingsEvent() + + data class OnUpdateCategoryTitle( + val id: Int, + val title: String + ) : SettingsEvent() + + data class OnUpdateCategoryOrder( + val categories: List + ) : SettingsEvent() + + data class OnRemoveCategory( + val category: Category + ) : SettingsEvent() + + data class OnUpdateCategorySort( + val categoryId: Int, + val sortOrder: LibrarySortOrder, + val sortOrderDescending: Boolean + ) : SettingsEvent() + data class OnSelectColorPreset( val id: ID ) : SettingsEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt index 246a15cf..dbab7eb5 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt @@ -20,7 +20,16 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.reader.ColorPreset +import ua.acclorite.book_story.domain.use_case.category.DeleteCategory +import ua.acclorite.book_story.domain.use_case.category.GetCategories +import ua.acclorite.book_story.domain.use_case.category.GetCategorySort +import ua.acclorite.book_story.domain.use_case.category.InsertCategory +import ua.acclorite.book_story.domain.use_case.category.UpdateCategoryOrder +import ua.acclorite.book_story.domain.use_case.category.UpdateCategorySort +import ua.acclorite.book_story.domain.use_case.category.UpdateCategoryTitle import ua.acclorite.book_story.domain.use_case.color_preset.DeleteColorPreset import ua.acclorite.book_story.domain.use_case.color_preset.GetColorPresets import ua.acclorite.book_story.domain.use_case.color_preset.ReorderColorPresets @@ -41,7 +50,14 @@ class SettingsModel @Inject constructor( private val reorderColorPresets: ReorderColorPresets, private val deleteColorPreset: DeleteColorPreset, private val grantPersistableUriPermission: GrantPersistableUriPermission, - private val releasePersistableUriPermission: ReleasePersistableUriPermission + private val releasePersistableUriPermission: ReleasePersistableUriPermission, + private val insertCategory: InsertCategory, + private val getCategories: GetCategories, + private val updateCategoryTitle: UpdateCategoryTitle, + private val updateCategoryOrder: UpdateCategoryOrder, + private val deleteCategory: DeleteCategory, + private val updateCategorySort: UpdateCategorySort, + private val getCategoriesSort: GetCategorySort, ) : ViewModel() { private val mutex = Mutex() @@ -85,7 +101,9 @@ class SettingsModel @Inject constructor( _state.update { it.copy( selectedColorPreset = colorPresets.selected(), - colorPresets = colorPresets + colorPresets = colorPresets, + categories = getCategories.execute(), // Getting categories + categoriesSort = getCategoriesSort.execute() ) } @@ -112,6 +130,78 @@ class SettingsModel @Inject constructor( } } + is SettingsEvent.OnCreateCategory -> { + viewModelScope.launch { + insertCategory.execute( + category = Category( + title = event.title + ) + ) + _state.update { + it.copy( + categories = getCategories.execute() + ) + } + } + } + + is SettingsEvent.OnUpdateCategoryTitle -> { + viewModelScope.launch { + updateCategoryTitle.execute( + id = event.id, + title = event.title + ) + _state.update { + it.copy( + categories = getCategories.execute() + ) + } + } + } + + is SettingsEvent.OnUpdateCategoryOrder -> { + viewModelScope.launch { + updateCategoryOrder.execute( + categories = event.categories + ) + _state.update { + it.copy( + categories = getCategories.execute() + ) + } + } + } + + is SettingsEvent.OnRemoveCategory -> { + viewModelScope.launch { + deleteCategory.execute( + category = event.category + ) + _state.update { + it.copy( + categories = getCategories.execute() + ) + } + } + } + + is SettingsEvent.OnUpdateCategorySort -> { + viewModelScope.launch { + updateCategorySort.execute( + categorySort = CategorySort( + categoryId = event.categoryId, + sortOrder = event.sortOrder, + sortOrderDescending = event.sortOrderDescending + ) + ) + _state.update { + it.copy( + categoriesSort = getCategoriesSort.execute() + ) + } + } + } + is SettingsEvent.OnSelectColorPreset -> { viewModelScope.launch { cancelColorPresetJobs() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt index 6c7f24eb..2e1e7955 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsScreen.kt @@ -37,6 +37,9 @@ object SettingsScreen : Screen, Parcelable { navigateToReaderSettings = { navigator.push(ReaderSettingsScreen) }, + navigateToLibrarySettings = { + navigator.push(LibrarySettingsScreen) + }, navigateToBrowseSettings = { navigator.push(BrowseSettingsScreen) }, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt index a1771ee8..218c0296 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsState.kt @@ -8,6 +8,8 @@ package ua.acclorite.book_story.ui.settings import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.domain.library.category.Category +import ua.acclorite.book_story.domain.library.category.CategorySort import ua.acclorite.book_story.domain.reader.ColorPreset import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset @@ -16,5 +18,8 @@ data class SettingsState( val colorPresets: List = emptyList(), val selectedColorPreset: ColorPreset = provideDefaultColorPreset(), val animateColorPreset: Boolean = false, - val colorPresetListState: LazyListState = LazyListState() + val colorPresetListState: LazyListState = LazyListState(), + + val categories: List = emptyList(), + val categoriesSort: List = emptyList(), ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeExtensions.kt b/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeExtensions.kt index 2cc1e6f9..4a6e62ee 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeExtensions.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/theme/ThemeExtensions.kt @@ -7,13 +7,11 @@ package ua.acclorite.book_story.ui.theme import androidx.compose.material3.ColorScheme -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color val ColorScheme.readerBarsColor: Color get() = surfaceContainer.copy(0.9f) -@Composable fun ColorScheme.dynamicListItemColor( index: Int ): Color { diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a14d951a..73966991 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -4,22 +4,20 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + التاريخ يمكنك اختيار أي الكُتب تريد إضافتها. يمكنك تعديل العنوان و صورة غلاف الكتاب لاحقا. عملية التحميل يمكن أن تستغرق حتى بضع دقائق. - اختر الفئة التي تريد نقل هذا الكتاب إليها. سيتم نقل هذا الكتاب إلى الفئة المُحدّدة. المكتبة تصفح الإعدادات عن التطبيق مساعدة إضافة الكُتب؟ - نقل الكُتب؟ حذف الكُتب؟ حذف الكتاب؟ - نقل الكتاب؟ حذف تاريخ القراءة؟ - اختر الفئة التي تريد نقل جميع الكُتب (%1$s) المُحدّدة. جميع الكُتب المُحدّدة سيتم نقلها إلى الفئة المُحدّدة. هذا سيحذف جميع الكُتب (%1$s) المُحدّدة من قاعدة البيانات. هذا لن يحذف الكُتب المُحدّدة مِن جهازك. هذا سيحذف هذا الكتاب من قاعدة البيانات. هذا لن يحذف الكتاب من جهازك. هذا سيحذف التاريخ بأكمله من قاعدة البيانات. بعد حذف التاريخ بأكمله، سيتم إعادة ضبط ترتيب الكُتب. @@ -69,8 +67,6 @@ حذف صورة الغلاف حذف صورة غلاف الكتاب الحالية المؤلف - يقرأ - يقرأ بالفعل عام الألوان أسود نقي @@ -88,7 +84,6 @@ متوسط بحث على الويب مشاركة - أخطط تصفية رتّب عام، تصفية، ترتيب @@ -102,7 +97,7 @@ قائمة شبكة تصفح - ترتيب + ترتيب لم يتم العثور على تطبيق قاموس. لم يتم العثور على تطبيق للمشاركة. مائل @@ -121,16 +116,16 @@ حُذف التاريخ بنجاح. تم إعادة تعيين صورة الغلاف بنجاح. عالي - اوتوماتيكي - بالسطر - أبجديا + اوتوماتيكي + بالسطر + أبجديا نبتون المريخ المشتري تم حذف هذا الكتاب بنجاح. تم نقل هذا الكتاب بنجاح. - صيغة الملف - حجم الملف + صيغة الملف + حجم الملف الارض نُقلت جميع الكُتب المُحدّدة بنجاح. أُضيفت جميع الكُتب المُحدّدة بنجاح. @@ -138,7 +133,7 @@ تم محو عناصر التاريخ بنجاح. الحجم غير معروف - حجم الشبكة + حجم الشبكة إصدار التطبيق رخص مفتوحة المصدر الأيقونات @@ -160,7 +155,7 @@ مرر الشريط العلوي لليسار أو اليمين لتغيير إعداد اللون المسبق تسمية إعداد مسبق… توفير - آخر تعديل + آخر تعديل مواءمة النص اضغط مرة أخرى مزدوجة للخروج اضغط مرتين للخروج من التطبيق @@ -185,7 +180,6 @@ إعداد اللون المسبق مظلم بالكامل تغيير لون الخلفية لتصبح سوداء بالكامل - مهمل قارئ متنوع الهوامش diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 56982a95..793839db 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -9,7 +9,6 @@ Выдаліць Гэта выдаліць усе выбраныя кнігі(%1$s) з базы дадзеных. Гэта не выдаліць выбраныя кнігі з вашай прылады. Выдаліць кнігі? - Выбярыце катэгорыю, у якую хочаце перамясціць ўсе выбраныя кнігі(%1$s). Усе выбраныя кнігі будуць перамешчаны ў выбраную катэгорыю. Слоўнік Ніколі Па змаўчанні @@ -20,13 +19,10 @@ Пра прыкладанне Даведка Дадаць кнігі? - Перамясціць кнігі? Выдаліць гісторыю чытання? Рэдагаваць Вы можаце выбраць кнігі, якія хочаце дадаць. Вы зможаце змяніць Назву і Відарыс Вокладкі кнігі пазней. Працэс загрузкі можа занять некалькі хвілін. - Перамясціць кнігу? Гэта выдаліць гэтую кнігу з базы дадзеных. Кніга не будзе выдалена з вашай прылады. - Выберыце катэгорыю, у якую вы хочаце перамясціць гэтую кнігу. Гэтая кніга будзе перамешчана ў выбраную катэгорыю. Гэта выдаліць усю гісторыю з базы дадзеных. Пасля выдалення ўсёй гісторыі парадак кніг будзе выдалены. Без апісання. Нажаль! Штосьці пайшло не так. @@ -98,7 +94,7 @@ Памер шрыфта Падлік прагрэсу Рэжым адлюстравання - Памер сеткі + Памер сеткі Чырвоны Зялёны Сіні diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 61550b11..c970eaf2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Tato kniha byla úspěšně smazána. Neptun Barva pozadí @@ -38,20 +40,18 @@ Vypnutý Historie Tmavé téma - Vyberte kategorii, do které chcete přesunout všechny vybrané knihy. (%1$s). Všechny vybrané knihy do ní budou přesunuty. Zrušit Nelze resetovat obálku. Naposledy otevřeno Filtr Dvojitým zpětným klepnutím ukončíte - Přesunout knihy? Ikony Nápady Smazat historii čtení? Neznámý Fonty Standardní - Abecedně + Abecedně Cesta k souboru Všechny vybrané knihy byly úspěšně přidány. Tato kniha byla úspěšně přesunuta. @@ -71,35 +71,31 @@ Vybráno «%1$s» Velikost souboru Téma aplikace - Řadit + Řadit Merkur Kurzíva Procházet Žádná aplikace pro sdílení nebyla nalezena. Procházet - Velikost mřížky + Velikost mřížky Smazat knihy? Normální Žádná aplikace pro překladač nebyla nalezena. Resetovat obálku knihy na výchozí Všechny vybrané videa byly úspěšně smazány. - Naposledy změněno + Naposledy změněno Přejít na - Právě čtu Smaže to celou historii z databáze. Po smazaní z historie bude pořadí knih resetováno. Knihovna - Automaticky + Automaticky Smazat obálku Jazyk aplikace Eris Autor - Vyberte kategorii, do které chcete přesunout tuto knihu. Vybraná kniha do ní bude přesunuta. - Budu číst Pomoc Žádná aplikace pro slovník nebyla nalezena. %1$s vybráno Open source licence - Opuštěné Přesunout Smazat Zelená @@ -107,8 +103,8 @@ Celá obrazovka Překlad na dvojité kliknutí Nechat obrazovku zapnutou - Formát souboru - Velikost souboru + Formát souboru + Velikost souboru Začátek Konec Na výšku @@ -132,7 +128,6 @@ Procházet O aplikaci Přidat knihy? - Přesunout knihu? Smaže to tuto knihu z databáze. Nesmaže to na vašem zařízení. Žádný popis. Něco se pokazilo. @@ -147,7 +142,6 @@ Změnit obálku Vybrat novou obálku z galerie Smazat aktuální obálku knihy - Přečteno Obecné Barvy Řadit @@ -171,7 +165,7 @@ Spořič Střední Vysoký - na řádku + na řádku Zarovnat Do prostřed Jupiter diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5bc0bc14..1ba15b45 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -4,21 +4,19 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Bibliothek Verlauf Durchsuchen Einstellungen Über diese App - Bücher verschieben? Bücher löschen? Buch löschen? - Buch verschieben? Leseverlauf löschen? Sie können auswählen, welche Bücher Sie hinzufügen möchten. Titel und Coverbild des Buches können Sie später bearbeiten. Der Ladevorgang kann mehrere Minuten dauern. - Wählen Sie die Kategorie, in die Sie alle ausgewählten Bücher verschieben möchten (%1$s). Alle ausgewählten Bücher werden in die ausgewählte Kategorie verschoben. Dadurch wird das Buch aus der Datenbank gelöscht. Von Ihrem Gerät wird das Buch jedoch nicht gelöscht. - Wählen Sie die Kategorie aus, in die Sie dieses Buch verschieben möchten. Dieses Buch wird in die ausgewählte Kategorie verschoben. Keine Beschreibung. Die Datei ist leer, fehlt oder ist unzugänglich. Überprüfe die Datei, überprüfe die Einstellungen oder änder den Pfad und versuche es dann erneut. Titelbild konnte nicht zurückgesetzt werden. @@ -44,10 +42,6 @@ Titelbild löschen Aktuelles Coverbild des Buches löschen Author - Lesend - Schon gelesen - Geplant - Abgebrochen Allgemein Farben App-Sprache @@ -221,23 +215,23 @@ Durchsuchen Allgemein, Filter, Sortieren Filter - Sortieren + Sortieren Anzeigemodus - Rastergröße + Rastergröße Liste Raster - Auto - Pro Reihe - Alphabetisch - Dateiformat + Auto + Pro Reihe + Alphabetisch + Dateiformat Sortierung Alle Dateien auswählen Keine App zum Teilen gefunden. Keine Wörterbuch-App gefunden. Websuche Teilen - Zuletzt geändert - Dateigröße + Zuletzt geändert + Dateigröße Textausrichtung Start Blocksatz diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 07eedfd6..f5ab670e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Biblioteca Historial Navegar @@ -12,13 +14,9 @@ Acerca de la aplicación Ayuda ¿Añadir libros? - ¿Mover libros? ¿Borrar libros? ¿Borrar libro? - ¿Mover libro? ¿Borrar el historial de lectura? - Elija la categoría a la que desea mover todos los libros seleccionados (%1$s). Todos los libros seleccionados serán trasladados a la categoría seleccionada. - Elija la categoría a la que quiere mover este libro. Este libro será trasladado a la categoría seleccionada. Eliminará todo el historial de la base de datos. Después de borrar todo el historial, el orden de los libros se restablecerá. Sin descripción. Algo salió mal. @@ -31,13 +29,10 @@ Cambiar portada Elegir nueva portada de sus fotos Borrar la portada actual del libro - Leyendo - Leído Colores General Apariencia Tema, colores - Abandonado Lenguaje de la aplicación Tema oscuro Oscuro puro @@ -97,7 +92,6 @@ Reiniciar portada Reiniciar la portada del libro a la predeterminada Autor - Planeado Lenguaje de la aplicación Lector Fuente, texto, traductor @@ -216,10 +210,10 @@ Ve a la configuración de Apariencia (consulta: ¿Cómo personalizo Reader?). Puede elegir qué colores preestablecidos usar en la configuración «Ajuste preestablecido de color». Haz clic en el ajuste preestablecido de color deseado para seleccionarlo; esto también cambiará tus colores en Reader. Si deseas reordenar el ajuste preestablecido de color, selecciona lo y manten presionado el botón con el ícono «Manejar»; luego, muévelo a la posición deseada. Puedes cambiar rápidamente los ajustes preestablecidos de color habilitando la configuración correspondiente en la subcategoría «Colores» de la configuración de Apariencia y luego deslizando la barra superior hacia la izquierda o la derecha en Reader para cambiar rápidamente el ajuste preestablecido de color. Tryzub Clasificar - Tamaño de la cuadrícula + Tamaño de la cuadrícula Cuadrícula - por fila - Tamaño del archivo + por fila + Tamaño del archivo Orden de clasificación No se ha encontrado ninguna aplicación para compartir. No se ha encontrado ninguna aplicación de diccionario. @@ -229,13 +223,13 @@ Navegar General, filtrar, clasificar Filtrar - Clasificar + Clasificar Modo de visualización Lista - Auto - Formato del archivo - Alfabéticamente - Última modificación + Auto + Formato del archivo + Alfabéticamente + Última modificación Seleccionar todos los archivos Filtrar Alineación del texto diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3caee420..bd1a2ed8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Bibliothèque Historique Naviguer @@ -12,14 +14,11 @@ À propos de l\'application Aide Supprimer des livres ? - Déplacer des livres ? Supprimer le livre ? - Déplacer le livre ? Effacer l\'historique de lecture ? Vous pouvez choisir les livres que vous souhaitez ajouter. Vous pouvez modifier le titre et l\'image de couverture du livre par la suite. Le chargement peut prendre plusieurs minutes. Cela supprimera tous les livres sélectionnés (%1$s) de la base de données. Ceci ne supprimera pas les livres sélectionnés de votre appareil. Cela supprimera ce livre de la base de données. Ceci ne le supprimera pas de votre appareil. - Choisissez la catégorie dans laquelle vous souhaitez déplacer ce livre. Ce dernier sera déplacé vers la catégorie choisie. Aucune application de traduction n\'a été trouvée. Aucune application de dictionnaire n\'a été trouvée. Aucune application pour partager n\'a été trouvée. @@ -51,10 +50,6 @@ Supprimez l\'image de couverture Supprimer l\'image actuelle de la couverture du livre Auteur - Lecture en cours - Déjà lu - À lire - Abandonné Général Filtres Trier @@ -83,7 +78,7 @@ Alignement du texte Espacement des lettres Mode d\'affichage - Taille de la grille + Taille de la grille Rouge Vert Bleu @@ -99,10 +94,10 @@ Normal Liste Grille - Automatique - par ligne - Dernière modification - Taille de fichier + Automatique + par ligne + Dernière modification + Taille de fichier Début Justifier Centrer @@ -204,7 +199,6 @@ Mélanger les couleurs Tryzub Glisser - Choisissez la catégorie dans laquelle vous souhaitez déplacer tous les livres sélectionnés (%1$s). Tous les livres sélectionnés seront déplacés vers la catégorie choisie. Ajouter des livres ? Pas de description. Quelque chose s\'est mal passé. @@ -216,7 +210,7 @@ Ajouter un livre Couleurs Paramètres généraux - Trier + Trier Langue de l\'application Thème sombre Noir pur @@ -231,8 +225,8 @@ Hauteur de la ligne Activé Désactivé - Format de fichier - Alphabétiquement + Format de fichier + Alphabétiquement Version de l\'application Design Traduction diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 686a32da..cc5ab352 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + पुस्तकालय इतिहास सेटिंग्स @@ -12,9 +14,7 @@ डिफ़ॉल्ट सहायता किताबें शामिल करें ? - किताबें स्थानांतरित करें? किताब हटाएं? - वह श्रेणी चुनें जिसमें आप सभी चयनित पुस्तकों को ले जाना चाहते हैं(%1$s)। सभी चयनित पुस्तकें चयनित श्रेणी में ले जाई जाएँगी। कुछ गड़बड़ हुआ है । कोई अनुवादक ऐप नहीं मिला । शेर करने के लिए कोई ऐप नहीं मिला । @@ -42,10 +42,6 @@ कवर छवि मिटाएँ पुस्तक का वर्तमान कवर चित्र मिटाएं लेखक - पढ़ रहे हैं - पढ़ चुके हैं - योजना - गिरा दिया सामान्य रंग फ़िल्टर @@ -63,10 +59,9 @@ पढ़ने की गति थीम सेटिंग्स रंगों - क्रमबद्ध करना + क्रमबद्ध करना ऐप भाषा किताबें हटाएँ? - किताब को स्थानांतरित करें ? और देखें पढ़ने का इतिहास मिटाएँ? ऐप भाषा @@ -78,7 +73,6 @@ आप चुन सकते हैं कि आप कौन सी किताबें जोड़ना चाहते हैं। आप बाद में किताब का शीर्षक और कवर छवि संपादित कर सकते हैं। लोडिंग प्रक्रिया में कई मिनट लग सकते हैं। यह डेटाबेस से सभी चयनित पुस्तकों(%1$s) को हटा देगा। यह आपके डिवाइस से चयनित पुस्तकों को नहीं हटाएगा। यह इस पुस्तक को डेटाबेस से हटा देगा. यह आपकी डिवाइस से इस पुस्तक को नहीं हटाएगा। - वह श्रेणी चुनें जिसमें आप इस पुस्तक को ले जाना चाहते हैं। यह पुस्तक चयनित श्रेणी में ले जाई जाएगी। यह डेटाबेस से पूरा इतिहास मिटा देगा। पूरा इतिहास मिटाने के बाद, किताबों का क्रम फिर से सेट हो जाएगा। कोई वर्णन नहीं। कोई शब्दकोश ऐप नहीं मिला । @@ -116,7 +110,7 @@ केंद्र पर ध्यान केंद्रित करके अपनी पढ़ने की गति बढ़ाएँ धारणा विस्तारक लाइन पैडिंग प्रदर्शन मोड - ग्रिड का आकार + ग्रिड का आकार हरा नीला प्रीसेट को नाम दें… @@ -133,9 +127,9 @@ साधारण सूची ग्रिड - स्वचालित - फाइल का प्रकार - आखिरी बार संशोधित + स्वचालित + फाइल का प्रकार + आखिरी बार संशोधित प्रारंभ सममित करें मध्य @@ -157,9 +151,9 @@ कवर छवि सफलतापूर्वक बदल दी गई। यह पुस्तक सफलतापूर्वक स्थानांतरित कर दी गई। कॉपी हो चुका - प्रति पंक्ति - वर्णक्रम - फ़ाइल साइज़ + प्रति पंक्ति + वर्णक्रम + फ़ाइल साइज़ निःशुल्क शनि ग्रह एरीस diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8ee11356..376d5a2a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Biblioteca Cronologia Predefinito @@ -30,11 +32,10 @@ Impostazioni lettore File Criterio di ordinamento - Dimensioni griglia + Dimensioni griglia Apri sul web Eliminare libro? Icona app - Scegli la categoria in cui spostare tutti i libri selezionati (%1$s). Tutti i libri selezionati saranno spostati nella categoria selezionata. Urano Mai Aggiungi un libro @@ -78,9 +79,9 @@ Preset colori Indentazione paragrafo Rosso - Auto + Auto Disabilitato - Formato file + Formato file Tutti i libri selezionati sono stati eliminati correttamente. Verticale Tutti i libri selezionati sono stati aggiunti correttamente. @@ -100,9 +101,9 @@ Carattere Tema applicazione Standard - Alfabetico - Ultima modifica - Dimensioni file + Alfabetico + Ultima modifica + Dimensioni file Saturno Mercurio Plutone @@ -118,7 +119,6 @@ Aiuta a tradurre Venere Dizionario - Scegli la categoria in cui spostare questo libro. Questo libro sarà spostato nella categoria selezionata. Contrasto tema Cerca Terra @@ -126,10 +126,8 @@ Autore Lista Traduzione - Spostare libro? Impostazioni Aggiungere libri? - Spostare libri? Dimensione carattere Blu Medio @@ -171,16 +169,15 @@ %1$s selezionato Luminosità personalizzata Luminosità - Ordinamento + Ordinamento Famiglia di font Libreria Benvenuti in Book\'s Story! - per riga + per riga La cronologia è stata eliminata correttamente. Premere di nuovo Indietro per uscire Dimensione Sfoglia - Già letto Casella di controllo Come posso creare e modificare le preimpostazioni di colore? Slava Ukraini! @@ -205,7 +202,6 @@ Seleziona tutti i file Impostazioni Elimina cronologia elemento - Lettura Come si legge un libro? Cancella gli elementi selezionati Ripristina @@ -228,9 +224,7 @@ Cambia immagine di copertina Mescola i colori Elimina l\'immagine di copertina corrente del libro - Pianificazione Preselezione %1$s - Abbandonato Cambia il colore dello sfondo in completamente nero Margini verticali Tutto fatto! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ddfa760e..5b516e3a 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -10,11 +10,8 @@ アプリについて ヘルプ 書籍の追加? - 書籍の移動? 書籍の削除? - 書籍の移動? 読書履歴を消去しますか? - 選択したすべての書籍(%1$s)を移動するカテゴリを選択します。 選択したすべての書籍を選択したカテゴリに移動します。 データベースから選択したすべての書籍(%1$s)を削除します。 選択した書籍はデバイスから削除されません。 すべての履歴をデータベースから削除します。すべての履歴が削除されると、書籍の順序はリセットされます。 問題が発生しました。 @@ -45,7 +42,6 @@ カバー画像を変更する カバー画像を消去する 著者 - 計画 一般 並び替え リーダー @@ -69,7 +65,6 @@ スキャン 表示 フィルタ - 並び替え カバー画像をリセットできません。 編集 書籍の削除? @@ -93,8 +88,6 @@ 翻訳 パスを変更 フォルダを追加 - 閲覧中 - 既読 カラー フィルタ テーマ、カラー @@ -102,7 +95,6 @@ アプリの言語 読書速度 ダークテーマ - この本を移動させたいカテゴリを選択してください。本は選択されたカテゴリに移動されます。 ライブラリ ブラウズ 真っ黒(OLED) @@ -141,9 +133,6 @@ ハイライトの太さ イタリック ノーマル - 自動 - 毎行 - アルファベット順 グレースケール 背景色 @@ -179,7 +168,6 @@ 配置 フォントサイズ 進捗状況カウント - 格子サイズ プリセット名… @@ -190,9 +178,6 @@ セーバー スタンダード - ファイル形式 - 最終更新 - ファイルサイズ 表示形式 リスト グリッド @@ -299,7 +284,6 @@ 履歴はどうやって管理するのですか? ツールチップはどうやって使いますか? 外観設定に移動します(参照:リーダーをカスタマイズするにはどうすればいいですか?)。すべてのカラープリセットの設定を変更する場合、「カラー」カテゴリのサブカテゴリである『カラープリセット』の下のボックスを使用します。新しいカラープリセットを作成するには、「追加」アイコンボタンをクリックします。プリセットを変更する場合は複数の方法があります。テキストフィールドをクリックするとカラープリセットの名前を変更でき、スライダーを動かすとプリセットの色を変えることが出来ます。また、「シャッフル」ボタンをクリックするとすべての色をシャッフルすることが出来ます。カラープリセットを削除する場合は「削除」アイコンボタンをクリックします。 - 中断 表示 切り抜き余白 知覚拡張 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 9e4ad868..a2afb396 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + ആപ്പിനെക്കുറിച്ച് സഹായം ക്രമീകരണങ്ങൾ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8cdb008f..a5d217b8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Historia Pomoc Przenieś @@ -33,7 +35,7 @@ Przeglądaj Anuluj Usuń - Sortuj + Sortuj Czerwony Ogólne Wygląd @@ -42,10 +44,9 @@ Włączono Merkury Neptun - Wybierz kategorię, do której chcesz przenieść tę książkę. Ta książka zostanie przeniesiona do wybranej kategorii. Zresetuj obraz okładki książki do domyślnego Mars - Alfabetycznie + Alfabetycznie Właczony Styl czcionki Odstępy między literami @@ -53,15 +54,12 @@ Tak, przejdź do Pomoc Usuń obraz okładki Pluton - Planowanie Szybka zmiana ustawienia koloru O aplikacji Dodać książki? Usunąć książki? Usunąć książkę? - Przenieść książkę? Usunąć historię czytania? - Wybierz kategorię, do której chcesz przenieść wszystkie wybrane książki (%1$s). Wszystkie wybrane książki zostaną przeniesione do wybranej kategorii. To spowoduje usunięcie tej książki z bazy danych, ale nie usunie jej z twojego urządzenia. To spowoduje usuniecie całej historii z bazy danych. Po usunięciu całej historii kolejność książek zostanie zresetowana. Brak opisu. @@ -75,8 +73,6 @@ Wybrano %1$s Wyszukiwanie w sieci Zmień obraz okładki - Czytanie - Przeczytano Język aplikacji Różne Ciemny motyw @@ -102,9 +98,9 @@ Średni Normalny Lista - Format pliku - Ostatnio modyfikowany - Rozmiar pliku + Format pliku + Ostatnio modyfikowany + Rozmiar pliku Start Uzasadnij Koniec @@ -117,12 +113,10 @@ Siatka Standardowy Zresetuj obraz okładki - Przenieść książki? Nie można zresetować obrazu okładki. To spowoduje usunięcie wszystkie wybranych książek (%1$s) z bazy danych. Nie usunie wybranych książek z twojego urządzenia. - na rząd + na rząd Wyrównanie tekstu - Porzucone Tryb wyświetlania Możesz wybrać książki, które chcesz dodać. Możesz później edytować tytuł i obraz okładki książki. Proces ładowania może potrwać do kilku minut. Nie znaleziono aplikacji przeglądarki. @@ -143,7 +137,7 @@ Preset %1$s Usuń bieżący obraz okładki książki Wybrano «%1$s» - Automatyczny + Automatyczny Wszystkie wybrane książki zostały pomyślnie dodane. Jak mogę dostosować aplikację? Witamy w Book\'s Story! @@ -168,7 +162,7 @@ Kontynuuj czytanie Zacznij czytać książkę (patrz: Jak mogę przeczytać książkę?), A następnie przynieś górne i dolne paski, klikając w dowolnym miejscu na ekranie. Następnie kliknij ikonę zębatki w prawym górnym rogu. Spowoduje to wyświetlenie wszystkich ustawień Czytnika. Drugim sposobem jest przejście do Nazwij ustawienie… - Rozmiar siatki + Rozmiar siatki Kolejność sortowania Nie znaleziono obrazu okładki Wnieśli wkład diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4e126ab8..ce042d00 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -4,16 +4,17 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Vermelho - Mover livro? Apagar livro? Padrão Cancelar Mover Nunca Cores - Organizar + Organizar Verde Médio Alto @@ -30,7 +31,6 @@ Configurações Sobre o app Ajuda - Mover livros? Você pode escolher que livros adicionar. Você pode editar o título e a capa do livro depois. O processo pode levar alguns minutos. Copiar Traduzir @@ -38,9 +38,6 @@ Próximo Compartilhar Desconhecido - Lendo - Planejando - Abandonado Cores Filtro Geral @@ -71,7 +68,7 @@ Azul Grade Hoje - Automático + Automático Começar Fim Mercúrio @@ -104,13 +101,12 @@ Pesquisar Copiado Justificar - Escolha a categoria da qual você quer mover todos os livros selecionados(%1$s). Todos os livros selecionados serão movidos para essa categoria. Isto irá apagar todos os livros selecionados(%1$s) da base de dados. Isso não irá apagar os livros do seu dispositivo. Centro Configurações Reverter Configurações - Alfabeticamente + Alfabeticamente Selecionado Seta Design @@ -124,7 +120,6 @@ Tema, cores Linguagem do aplicativo Tema escuro - Escolha a categoria para qual você quer mover este livro. Esse livro será movido para tal categoria. Isto irá deletar todo o histórico da base de dados. Após deletá-lo, a ordem dos livros será restaurada. Recuo de parágrafo Espaçamento das letras @@ -150,18 +145,17 @@ Padrão de cores Margem do recorte Modo de exibição - por linha - Lido - Tamanho da grade + por linha + Tamanho da grade Sem descrição. %1$s selecionado Código fechado Pesquisa web Margens laterais Iluminação - Formato de arquivo - Modificação mais recente - Tamanho do arquivo + Formato de arquivo + Modificação mais recente + Tamanho do arquivo Livre Retrato Paisagem diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 9fc4d10f..547aa714 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Standard Marte Capitole @@ -72,26 +74,18 @@ Adaugă fișiere Makemake Pluto - Mutați cărțile? - Planificate Mergeți la Ștergeți cărțile? - Deja citite - Abandonate Io Cum gestionez istoricul? Adăugați cărțile? Culori și selectați categoria cititor pentru a personaliza setările generale sau categoria aspect pentru a personaliza setările culorilor. - Se citesc Puteți alege ce cărți doriți să adăugați. Puteți edita titlul și imaginea de copertă a cărții mai târziu. Procesul de încărcare poate dura până la câteva minute. Va șterge întreaga istorie din baza de date. După ștergerea întregii istorii, ordinea cărților va fi resetată. Cum personalizez cititorul de cărți? A intervenit o problemă. - Alegeți categoria în care doriți să mutați această carte. Cartea va fi mutată în categoria selectată. Gata - Mutați cartea? - Alegeți categoria în care doriți să mutați toate cărțile selectate (%1$s). Toate cărțile selectate vor fi mutate în categoria selectată. Va șterge toate cărțile selectate (%1$s) din baza de date. Nu va șterge cărțile selectate de pe dispozitivul dumneavoastră. Cum mut sau șterg cărți? Ștergeți istoricul lecturilor? @@ -133,16 +127,13 @@ Configurați scanarea Citiți cărțile preferate și sortați-le pe categorii Dimensiune - Alfabetic Locație Ultima dată deschis Editează câmpul Spațiere între litere Ați finalizat cu succes setarea de bază a aplicației. Doriți să învățați cum să utilizați aplicația citind ecranul de ajutor? Cititor - Dimensiunea fișierului Scanare - pe rând Mod de afișare Preferințe de scanare Mutați cărțile selectate diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index c28d73ed..2ce6029f 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -4,13 +4,14 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + பின்னிழுப்பு, பின்னிழுவிசை வடிப்பி நூலகம் வரலாறு புத்தகங்களைச் சேர்க்கவா? - இந்த புத்தகத்தை நீங்கள் நகர்த்த விரும்பும் வகையைத் தேர்வுசெய்க. இந்த நூல் தேர்ந்தெடுக்கப்பட்ட வகைக்கு மாற்றப்படும். ஏதோ தவறு நடந்தது. மூடிய மூல உங்கள் பதிவிறக்கங்களுடன் நூலகத்தின் புத்தக சேகரிப்பை விரிவாக்குங்கள் @@ -95,7 +96,7 @@ பங்கு ஒருபோதும் காட்சி முறை - கோப்பு வடிவம் + கோப்பு வடிவம் இயல்புநிலை வாசிப்பு வரலாற்றை நீக்கவா? விளக்கம் இல்லை. @@ -138,8 +139,8 @@ புலனுணர்வு விரிவாக்க வரி தடிமன் பட்டியல் வலைவாய் - தானி - ஒரு வரிசையில் + தானி + ஒரு வரிசையில் நடுவண் நான் ஒரு புத்தகத்தை எவ்வாறு திருத்துவது? நூலகம் @@ -170,12 +171,9 @@ அமைப்புகள் பயன்பாடு பற்றி உதவி - புத்தகங்களை நகர்த்தவா? புத்தகங்களை நீக்கவா? புத்தகத்தை நீக்கவா? - புத்தகத்தை நகர்த்தவா? நீங்கள் எந்த புத்தகங்களைச் சேர்க்க விரும்புகிறீர்கள் என்பதை நீங்கள் தேர்வு செய்யலாம். புத்தகத்தின் தலைப்பு மற்றும் படத்தை பின்னர் திருத்தலாம். ஏற்றுதல் செயல்முறை பல நிமிடங்கள் வரை ஆகலாம். - தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நகர்த்த விரும்பும் வகையைத் தேர்வுசெய்க. தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களும் தேர்ந்தெடுக்கப்பட்ட வகைக்கு மாற்றப்படும். இது தரவுத்தளத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நீக்கிவிடும். இது உங்கள் சாதனத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட புத்தகங்களை நீக்காது. இது முழு வரலாற்றையும் தரவுத்தளத்திலிருந்து நீக்குகிறது. முழு வரலாற்றையும் நீக்கிய பிறகு, புத்தகங்களின் வரிசை மீட்டமைக்கப்படும். இது இந்த புத்தகத்தை தரவுத்தளத்திலிருந்து நீக்கிவிடும். இது உங்கள் சாதனத்திலிருந்து இந்த புத்தகத்தை நீக்காது. @@ -190,13 +188,9 @@ கவர் படத்தை நீக்கு புத்தகத்தின் தற்போதைய அட்டை படத்தை நீக்கு நூலாசிரியர் - படித்தல் - ஏற்கனவே படித்தார் - திட்டமிடல் - கைவிடப்பட்டது பொது வடிப்பி - வரிசைப்படுத்து + வரிசைப்படுத்து தூய இருண்ட முழுமையான இருண்ட கருப்பொருள் மாறுபாடு @@ -211,7 +205,7 @@ வண்ண முன்னமைவை மாற்ற மேல் பட்டியை இடது அல்லது வலதுபுறமாக ச்வைப் செய்யவும் கடிதம் இடைவெளி திரை நோக்குநிலை - கட்டம் அளவு + கட்டம் அளவு சிவப்பு பச்சை நீலம் @@ -220,9 +214,9 @@ உயர்ந்த சாய்வு சாதாரண - அகரவரிசை - கடைசியாக மாற்றப்பட்டது - கோப்பு அளவு + அகரவரிசை + கடைசியாக மாற்றப்பட்டது + கோப்பு அளவு தொடங்கு நியாயப்படுத்துங்கள் பூட்டப்பட்ட உருவப்படம் diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a1ac112c..2751be34 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -4,7 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> + --> + + Varsayılan @@ -16,10 +18,8 @@ Yardım Kitaplar eklensin mi? - Kitaplar taşınsın mı? Kitaplar silinsin mi? Kitap silinsin mi? - Kitap taşınsın mı? Okuma geçmişi silinsin mi? Düzenle @@ -28,10 +28,6 @@ Kitabın başlığını ve kapak resmini daha sonra düzenleyebilirsiniz. Yükleme işlemi birkaç dakika sürebilir. - - Seçili kitapları (%1$s) taşımak istediğiniz kategoriyi seçin. - Tüm seçili kitaplar belirlenen kategoriye taşınacaktır. - Seçili kitaplar (%1$s) veritabanından silinecektir. Ancak cihazınızdaki kitaplar silinmeyecektir. @@ -40,10 +36,6 @@ Bu kitap veritabanından silinecektir. Ancak cihazınızdan silinmeyecektir. - - Bu kitabı taşımak istediğiniz kategoriyi seçin. - Kitap belirlenen kategoriye taşınacaktır. - Tüm geçmiş veritabanından silinecektir. Geçmişi sildikten sonra kitapların sıralaması sıfırlanacaktır. @@ -108,10 +100,6 @@ Kitabın mevcut kapak resmini sil Yazar - Okunan - Okundu - Planlanan - Bırakılan Genel Renkler Filtre @@ -141,7 +129,7 @@ Tema tercihleri Renkler Filtre - Sırala + Sırala Uygulama dili @@ -209,7 +197,7 @@ İlerleme sayacı Görüntüleme modu - Izgara boyutu + Izgara boyutu Kırmızı Yeşil @@ -234,13 +222,13 @@ Liste Izgara - Otomatik - satır başına + Otomatik + satır başına - Alfabetik - Dosya formatı - Son değiştirilme - Dosya boyutu + Alfabetik + Dosya formatı + Son değiştirilme + Dosya boyutu Başlangıç İki yana yasla diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 82916982..8ace8e79 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -19,20 +19,19 @@ Додати книги? - Перемістити книги? Видалити книги? Видалити книгу? - Перемістити книгу? Видалити історію читання? - Редагувати + Видалити категорію? Ви можете вибрати книги, які хочете додати. Ви зможете відредагувати Назву та Обкладинку книги пізніше. Процес завантаження може зайняти декілька хвилин. - Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s). Всі обрані книги будуть переміщені в обрану категорію. Це видалить всі обрані книги(%1$s) з бази даних. Це не видалить обрані книги з вашого пристрою. Це видалить цю книгу з бази даних. Це не видалить цю книгу з вашого пристрою. - Виберіть категорію, в яку ви хочете перемістити цю книгу. Ця книга буде переміщена в обрану категорію. Це видалить всю історію з бази даних. Після видалення всієї історії порядок книг буде скинутий. + + Це видалить цю категорію з Бібліотеки, але не видалить книги. + Немає опису. @@ -49,12 +48,14 @@ Не вдалося скинути обкладинку. Закритий код Файла не існує або він недоступний + Це поле не повинно бути пустим. Пошук… Розширюйте книжковий арсенал бібліотеки своїми завантаженнями Читайте ваші улюблені книги та сортуйте їх між категоріями Нехай ваша остання прочитана книга відкриє перед вами сторінки нової великої історії вашого читацького шляху + Немає категорій. %1$s обрано @@ -81,6 +82,8 @@ ОК Змінити шлях Додати теку + Редагувати + Створити категорію Налаштувати сканування @@ -94,10 +97,7 @@ Автор - Читаю - Прочитано - Планую - Кинуто + Типово Головні Вигляд Кольори @@ -111,9 +111,11 @@ Зовнішній вигляд Тема, кольори Читач - Шрифт, текст, перекладач + Шрифт, текст Огляд - Головні, фільтр, сортування + Сканування, вигляд + Бібліотека + Категорії, вигляд Шрифт @@ -131,8 +133,10 @@ Кольори Сканування Вигляд + Вкладки + Категорії Фільтр - Сотрувати + Сотрувати @@ -140,6 +144,12 @@ Видалення теки тут не видалить фактичні файли чи книги з Бібліотеки. Щоб відкликати доступ, натисніть відповідну кнопку. + + Спочатку книги додаються до категорії за замовчуванням, + але можуть належати до кількох користувацьких категорій створених тут. + Видалення категорії тут не видалить книги з Бібліотеки. + Затисніть і перетягніть іконку категорії, щоб змінити порядок категорій. + @@ -214,8 +224,14 @@ Розмір шрифта Підрахунок прогресу - - Кількість комірок + + Показувати кнопку «Читати» + Показувати прогрес + Розміщення заголовка + Показувати кількість книжок + Показувати вкладки категорій + Завжди показувати типову вкладку + Сортування за категоріями Червоний @@ -242,20 +258,32 @@ Похилий Нормальний - - Авто - кожен рядок + + Схований + Знизу + Всередині - - За алфавітом - Формат файла - Остання зміна - Розмір файла + + Заголовок Режим відображення Список - СіткаW + Сітка + + + За алфавітом + Формат файла + Остання зміна + Розмір файла + Останнє читання + Прогрес + Автор + + + Кількість комірок + Авто + кожен рядок Старт @@ -470,7 +498,7 @@ Закріпити Показати менше Показати більше - Редагувати поле + Редагувати Розгорнути Згорнути Прибрати diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fa799119..20ad346c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -4,8 +4,9 @@ ~ Book's Story — free and open-source Material You eBook reader. ~ Copyright (C) 2024-2025 Acclorite ~ SPDX-License-Identifier: GPL-3.0-only - --> - 移动书籍? + --> + + 删除阅读历史? 删除书籍? @@ -25,10 +26,8 @@ 历史 浏览 关于 app - 移动书籍? 所有已选择的书(%1$s)将会从数据库中删除,但不会从您的设备上删除。 这本书将会从数据库中删除,但不会从您的设备上删除。 - 选择你想将这本书移动到的分类。这本书将会被移动到指定的分类。 所有的历史将从数据库中被删除。删除所有的历史后,书的顺序将会被重置。 无简介。 出现了一些错误。 @@ -46,8 +45,6 @@ 未知 修改封面图片 删除这本书当前的封面图片 - 正在阅读 - 已阅读 添加书籍? 未发现浏览器应用。 添加一本书 @@ -62,14 +59,13 @@ 已选择 %1$s 删除书籍? 您可以挑选您想添加哪些书。您可以在之后编辑书的标题与封面图片。加载过程可能会花费数分钟。 - 选择你想将所选的书(%1$s)移动到的分类。所有被选择的书将会被移动到指定的分类。 阅读速度 暗黑主题 绝对黑暗 改变背景颜色使完全黑暗 刘海边距 将边距应用于刘海区域 - 网格大小 + 网格大小 系统 正常 已禁用 @@ -97,10 +93,9 @@ 应用主题 颜色 翻译器 - 排序 + 排序 蓝色 段落间距 - 已放弃 颜色 过滤 应用语言 @@ -123,7 +118,6 @@ 字体颜色 过滤 阅读器 - 计划中 斜体 红色 排序 @@ -140,8 +134,8 @@ 默认 屏幕方向 列表 - 每行 - 文件格式 + 每行 + 文件格式 开头 末尾 自由 @@ -156,20 +150,20 @@ 天王星 再次点击返回退出 系统 - 字母顺序 + 字母顺序 竖屏 土星 居中 水星 阋神星 - 文件大小 - 最后修改 + 文件大小 + 最后修改 常规 阅读器 常规 常规,过滤,排序 网格 - 自动 + 自动 成功添加所有已选择的书。 成功删除所有已选择的书。 成功删除了封面图片。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5eabee53..735d99b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,12 +20,10 @@ Add books? - Move books? Delete books? Delete book? - Move book? Delete reading history? - Edit + Remove category? @@ -33,10 +31,6 @@ You can edit Title and Cover image of the book later. The loading process can take up to several minutes. - - Choose the category to which you want to move all the selected books(%1$s). - All selected books will be moved to the selected category. - It will delete all selected books(%1$s) from the database. It will not delete selected books from your device. @@ -45,14 +39,13 @@ It will delete this book from the database. It will not delete this book from your device. - - Choose the category to which you want to move this book. - This book will be moved to the selected category. - It will delete the whole history from the database. After deleting the whole history, the order of the books will be reset. + + This will remove this category from the Library but will not delete any books. + No description. @@ -69,6 +62,7 @@ Could not reset cover image. Closed source File does not exist or inaccessible + This field should not be empty. @@ -83,6 +77,7 @@ Let your last book you read open the pages of a new great story of your reading journey + No categories. %1$s selected @@ -109,6 +104,8 @@ OK Change path Add folder + Edit + Create category Set up scanning @@ -122,10 +119,7 @@ Author - Reading - Already Read - Planning - Dropped + Default General Display Colors @@ -139,9 +133,11 @@ Appearance Theme, colors Reader - Font, text, translator + Font, text Browse - General, filter, sort + Scan, display + Library + Categories, display, tabs Font @@ -159,8 +155,10 @@ Colors Scan Display + Tabs + Categories Filter - Sort + Sort @@ -168,6 +166,12 @@ Removing a folder here will not delete actual files or books from the Library. To revoke access, click the corresponding button. + + Initially, books are added to the default category, + but can belong to several custom categories created here. + Removing a category here will not delete books from the Library. + Hold and move the category icon to rearrange the categories. + @@ -244,8 +248,14 @@ Font size Progress count - - Grid size + + Show «Read» button + Show progress + Title position + Show book count + Show category tabs + Always show default tab + Per-category sorting Red @@ -272,21 +282,33 @@ Italic Normal - - Auto - per row + + Hidden + Below + Inside - - Alphabetically - File format - Last modified - File size + + Title Display mode List Grid + + Alphabetically + File format + Last modified + File size + Last read + Progress + Author + + + Grid size + Auto + per row + Start Justify @@ -612,7 +634,7 @@ Pin Show less Show more - Edit field + Edit Expand Collapse Remove