feat(library): custom categories
* Custom categories * New customization options * Sorting Resolves: #79
This commit is contained in:
parent
e22e172ddb
commit
c70338a14d
126 changed files with 4521 additions and 722 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int>?): String {
|
||||
return json.encodeToString(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toCategoryList(value: String): List<Int> {
|
||||
return json.decodeFromString(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import javax.inject.Singleton
|
|||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCommonmarkParser(): Parser {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Int>
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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<CategoryEntity>
|
||||
|
||||
@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<CategorySortEntity>
|
||||
|
||||
@Query("DELETE FROM categorysortentity WHERE categoryId=:categoryId")
|
||||
suspend fun deleteCategorySortEntity(categoryId: Int)
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - */
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Category> {
|
||||
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<Category>) {
|
||||
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<CategorySort> {
|
||||
return database.getCategorySortEntities().map {
|
||||
categorySortMapper.toCategorySort(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int>
|
||||
) : Parcelable
|
||||
|
|
@ -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
|
||||
}
|
||||
data class Category(
|
||||
val id: Int = 0,
|
||||
val title: String,
|
||||
val order: Int = 0
|
||||
)
|
||||
|
|
@ -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<SelectableBook>
|
||||
data class CategorySort(
|
||||
val categoryId: Int,
|
||||
val sortOrder: LibrarySortOrder,
|
||||
val sortOrderDescending: Boolean
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<Category>
|
||||
|
||||
suspend fun updateCategoryTitle(
|
||||
id: Int,
|
||||
title: String
|
||||
)
|
||||
|
||||
suspend fun updateCategoriesOrder(
|
||||
categories: List<Category>
|
||||
)
|
||||
|
||||
suspend fun deleteCategory(
|
||||
category: Category
|
||||
)
|
||||
|
||||
suspend fun updateCategorySortEntity(
|
||||
categorySort: CategorySort
|
||||
)
|
||||
|
||||
suspend fun getCategorySortEntities(): List<CategorySort>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Category> {
|
||||
return repository.getCategories()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CategorySort> {
|
||||
return repository.getCategorySortEntities()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Category>) {
|
||||
repository.updateCategoriesOrder(categories)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Category>,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<Category>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Category>,
|
||||
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<Category>().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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -66,3 +66,14 @@ fun Char.isVisibleCharacter(): Boolean {
|
|||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> compareByWithOrder(
|
||||
descending: Boolean,
|
||||
selector: (T) -> Comparable<*>?
|
||||
): Comparator<T> {
|
||||
return if (descending) {
|
||||
compareByDescending(selector)
|
||||
} else {
|
||||
compareBy(selector)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Category>,
|
||||
showDefaultCategory: Boolean,
|
||||
pagerState: PagerState,
|
||||
categoriesSort: List<CategorySort>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectableBook>,
|
||||
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<CategoryWithBooks>,
|
||||
categories: List<Category>,
|
||||
showDefaultCategory: Boolean,
|
||||
perCategorySort: Boolean,
|
||||
categoriesSort: List<CategorySort>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<SelectableBook>,
|
||||
categories: List<CategoryWithBooks>,
|
||||
categories: List<Category>,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Category>,
|
||||
showDefaultCategory: Boolean,
|
||||
categoriesPagerState: PagerState,
|
||||
sortOrder: LibrarySortOrder,
|
||||
sortOrderDescending: Boolean,
|
||||
categoriesSort: List<CategorySort>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectableBook>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectableBook>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectableBook>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SelectableBook>,
|
||||
categories: List<CategoryWithBooks>,
|
||||
selectedItemsCount: Int,
|
||||
categories: List<Category>,
|
||||
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<Category>().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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SelectableBook>,
|
||||
pagerState: PagerState,
|
||||
categories: List<CategoryWithBooks>,
|
||||
categories: List<Category>,
|
||||
showDefaultCategory: Boolean,
|
||||
perCategorySort: Boolean,
|
||||
categoriesSort: List<CategorySort>,
|
||||
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<SelectableBook>.sortBooks(
|
||||
librarySortOrder: LibrarySortOrder,
|
||||
librarySortOrderDescending: Boolean
|
||||
): List<SelectableBook> {
|
||||
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<List<SelectableBook>>()
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SelectableBook>,
|
||||
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<CategoryWithBooks>,
|
||||
layout: LibraryLayout,
|
||||
gridSize: Int,
|
||||
autoGridSize: Boolean,
|
||||
categories: List<Category>,
|
||||
showDefaultCategory: Boolean,
|
||||
perCategorySort: Boolean,
|
||||
categoriesSort: List<CategorySort>,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<CategoryWithBooks>,
|
||||
categoriesWithBookCount: List<Pair<Category, Int>>,
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<SelectableBook>,
|
||||
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<CategoryWithBooks>,
|
||||
categories: List<Category>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SettingsModel>()
|
||||
val state = settingsModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val dialog = remember {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
val selectedCategory = remember {
|
||||
mutableStateOf<Category?>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MainModel>()
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Category>,
|
||||
val context: Context
|
||||
) : BookInfoEvent()
|
||||
|
||||
data object OnDismissDialog : BookInfoEvent()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BookInfoModel>()
|
||||
val settingsModel = hiltViewModel<SettingsModel>()
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
sortOrder: BrowseSortOrder
|
||||
): List<SelectableFile> {
|
||||
fun <T> compareByWithOrder(
|
||||
selector: (T) -> Comparable<*>?
|
||||
): Comparator<T> {
|
||||
return if (sortOrderDescending) {
|
||||
compareByDescending(selector)
|
||||
} else {
|
||||
compareBy(selector)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SelectableFile>.filterFiles(): List<SelectableFile> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<CategoryWithBooks>,
|
||||
val selectedCategories: List<Category>,
|
||||
val context: Context
|
||||
) : LibraryEvent()
|
||||
|
||||
|
|
@ -55,4 +53,8 @@ sealed class LibraryEvent {
|
|||
) : LibraryEvent()
|
||||
|
||||
data object OnDismissDialog : LibraryEvent()
|
||||
|
||||
data object OnShowFilterBottomSheet : LibraryEvent()
|
||||
|
||||
data object OnDismissBottomSheet : LibraryEvent()
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Long> = Channel(Channel.CONFLATED)
|
||||
|
||||
|
|
@ -61,34 +62,19 @@ object LibraryScreen : Screen, Parcelable {
|
|||
|
||||
val screenModel = hiltViewModel<LibraryModel>()
|
||||
val mainModel = hiltViewModel<MainModel>()
|
||||
val settingsModel = hiltViewModel<SettingsModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue