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
|
// EPUB parser
|
||||||
implementation("org.jsoup:jsoup:1.18.3")
|
implementation("org.jsoup:jsoup:1.18.3")
|
||||||
|
|
||||||
// FB2 parser
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.1")
|
|
||||||
|
|
||||||
// Language Switcher
|
// Language Switcher
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
implementation("androidx.appcompat:appcompat-resources:1.7.0")
|
implementation("androidx.appcompat:appcompat-resources:1.7.0")
|
||||||
|
|
@ -156,9 +153,9 @@ dependencies {
|
||||||
// Scrollbar
|
// Scrollbar
|
||||||
implementation("com.github.nanihadesuka:LazyColumnScrollbar:2.2.0")
|
implementation("com.github.nanihadesuka:LazyColumnScrollbar:2.2.0")
|
||||||
|
|
||||||
// Gson
|
|
||||||
implementation("com.google.code.gson:gson:2.11.0")
|
|
||||||
|
|
||||||
// Markdown
|
// Markdown
|
||||||
implementation("org.commonmark:commonmark:0.24.0")
|
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
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object AppModule {
|
object AppModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCommonmarkParser(): Parser {
|
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.local.data_store.DataStoreImpl
|
||||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
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.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.ColorPresetMapper
|
||||||
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl
|
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
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.TextParser
|
||||||
import ua.acclorite.book_story.data.parser.TextParserImpl
|
import ua.acclorite.book_story.data.parser.TextParserImpl
|
||||||
import ua.acclorite.book_story.data.repository.BookRepositoryImpl
|
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.ColorPresetRepositoryImpl
|
||||||
import ua.acclorite.book_story.data.repository.DataStoreRepositoryImpl
|
import ua.acclorite.book_story.data.repository.DataStoreRepositoryImpl
|
||||||
import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl
|
import ua.acclorite.book_story.data.repository.FileSystemRepositoryImpl
|
||||||
import ua.acclorite.book_story.data.repository.HistoryRepositoryImpl
|
import ua.acclorite.book_story.data.repository.HistoryRepositoryImpl
|
||||||
import ua.acclorite.book_story.data.repository.PermissionRepositoryImpl
|
import ua.acclorite.book_story.data.repository.PermissionRepositoryImpl
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
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.ColorPresetRepository
|
||||||
import ua.acclorite.book_story.domain.repository.DataStoreRepository
|
import ua.acclorite.book_story.domain.repository.DataStoreRepository
|
||||||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||||
|
|
@ -81,6 +87,12 @@ abstract class RepositoryModule {
|
||||||
permissionRepositoryImpl: PermissionRepositoryImpl
|
permissionRepositoryImpl: PermissionRepositoryImpl
|
||||||
): PermissionRepository
|
): PermissionRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCategoryRepository(
|
||||||
|
categoryRepositoryImpl: CategoryRepositoryImpl
|
||||||
|
): CategoryRepository
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindBookMapper(
|
abstract fun bindBookMapper(
|
||||||
|
|
@ -99,6 +111,18 @@ abstract class RepositoryModule {
|
||||||
colorPresetMapperImpl: ColorPresetMapperImpl
|
colorPresetMapperImpl: ColorPresetMapperImpl
|
||||||
): ColorPresetMapper
|
): ColorPresetMapper
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCategoryMapper(
|
||||||
|
categoryMapperImpl: CategoryMapperImpl
|
||||||
|
): CategoryMapper
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCategorySortMapper(
|
||||||
|
categorySortSortMapperImpl: CategorySortSortMapperImpl
|
||||||
|
): CategorySortMapper
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindFileParser(
|
abstract fun bindFileParser(
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.data.local.dto
|
package ua.acclorite.book_story.data.local.dto
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
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
|
@Entity
|
||||||
|
@TypeConverters(CategoryConverter::class)
|
||||||
data class BookEntity(
|
data class BookEntity(
|
||||||
@PrimaryKey(true) val id: Int = 0,
|
@PrimaryKey(true) val id: Int = 0,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
@ -21,5 +24,5 @@ data class BookEntity(
|
||||||
val scrollOffset: Int,
|
val scrollOffset: Int,
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
val image: String? = null,
|
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.Update
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
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.ColorPresetEntity
|
||||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||||
|
|
||||||
|
|
@ -93,4 +95,47 @@ interface BookDao {
|
||||||
@Query("DELETE FROM colorpresetentity")
|
@Query("DELETE FROM colorpresetentity")
|
||||||
suspend fun deleteColorPresets()
|
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.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
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.ColorPresetEntity
|
||||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -25,8 +27,10 @@ import java.io.File
|
||||||
BookEntity::class,
|
BookEntity::class,
|
||||||
HistoryEntity::class,
|
HistoryEntity::class,
|
||||||
ColorPresetEntity::class,
|
ColorPresetEntity::class,
|
||||||
|
CategoryEntity::class,
|
||||||
|
CategorySortEntity::class
|
||||||
],
|
],
|
||||||
version = 9,
|
version = 13,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(1, 2),
|
AutoMigration(1, 2),
|
||||||
AutoMigration(2, 3),
|
AutoMigration(2, 3),
|
||||||
|
|
@ -36,6 +40,10 @@ import java.io.File
|
||||||
AutoMigration(6, 7),
|
AutoMigration(6, 7),
|
||||||
AutoMigration(7, 8, spec = DatabaseHelper.MIGRATION_7_8::class),
|
AutoMigration(7, 8, spec = DatabaseHelper.MIGRATION_7_8::class),
|
||||||
AutoMigration(8, 9, spec = DatabaseHelper.MIGRATION_8_9::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
|
exportSchema = true
|
||||||
)
|
)
|
||||||
|
|
@ -113,4 +121,7 @@ object DatabaseHelper {
|
||||||
|
|
||||||
@DeleteTable("FavoriteDirectoryEntity")
|
@DeleteTable("FavoriteDirectoryEntity")
|
||||||
class MIGRATION_8_9 : AutoMigrationSpec
|
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(),
|
author = book.author.getAsString(),
|
||||||
description = book.description,
|
description = book.description,
|
||||||
image = book.coverImage?.toString(),
|
image = book.coverImage?.toString(),
|
||||||
category = book.category
|
categories = book.categories
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,8 +42,8 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
progress = bookEntity.progress,
|
progress = bookEntity.progress,
|
||||||
filePath = bookEntity.filePath,
|
filePath = bookEntity.filePath,
|
||||||
lastOpened = null,
|
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.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.book.BookWithCover
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
@ -91,7 +90,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
filePath = cachedFile.path,
|
filePath = cachedFile.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
categories = emptyList(),
|
||||||
coverImage = null
|
coverImage = null
|
||||||
),
|
),
|
||||||
coverImage = extractCoverImageBitmap(rawFile, coverImage)
|
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.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.book.BookWithCover
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -56,7 +55,7 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
filePath = cachedFile.path,
|
filePath = cachedFile.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
categories = emptyList(),
|
||||||
coverImage = null
|
coverImage = null
|
||||||
),
|
),
|
||||||
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.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.book.BookWithCover
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ class HtmlFileParser @Inject constructor() : FileParser {
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
filePath = cachedFile.path,
|
filePath = cachedFile.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
categories = emptyList(),
|
||||||
coverImage = null
|
coverImage = null
|
||||||
),
|
),
|
||||||
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.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.book.BookWithCover
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -47,7 +46,7 @@ class PdfFileParser @Inject constructor(
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
filePath = cachedFile.path,
|
filePath = cachedFile.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
categories = emptyList(),
|
||||||
coverImage = null
|
coverImage = null
|
||||||
),
|
),
|
||||||
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.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.book.BookWithCover
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -32,7 +31,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
filePath = cachedFile.path,
|
filePath = cachedFile.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
categories = emptyList(),
|
||||||
coverImage = null
|
coverImage = null
|
||||||
),
|
),
|
||||||
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 android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -30,5 +29,5 @@ data class Book(
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
|
|
||||||
val lastOpened: Long?,
|
val lastOpened: Long?,
|
||||||
val category: Category
|
val categories: List<Int>
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
@ -9,9 +9,8 @@ package ua.acclorite.book_story.domain.library.category
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
enum class Category {
|
data class Category(
|
||||||
READING,
|
val id: Int = 0,
|
||||||
ALREADY_READ,
|
val title: String,
|
||||||
PLANNING,
|
val order: Int = 0
|
||||||
DROPPED
|
)
|
||||||
}
|
|
||||||
|
|
@ -7,12 +7,11 @@
|
||||||
package ua.acclorite.book_story.domain.library.category
|
package ua.acclorite.book_story.domain.library.category
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
import ua.acclorite.book_story.domain.library.display.LibrarySortOrder
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CategoryWithBooks(
|
data class CategorySort(
|
||||||
val category: Category,
|
val categoryId: Int,
|
||||||
val title: UIText,
|
val sortOrder: LibrarySortOrder,
|
||||||
val books: List<SelectableBook>
|
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.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.BottomSheet
|
||||||
import ua.acclorite.book_story.domain.util.Dialog
|
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.BookInfoEvent
|
||||||
|
|
@ -16,6 +17,7 @@ import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoContent(
|
fun BookInfoContent(
|
||||||
book: Book,
|
book: Book,
|
||||||
|
categories: List<Category>,
|
||||||
bottomSheet: BottomSheet?,
|
bottomSheet: BottomSheet?,
|
||||||
dialog: Dialog?,
|
dialog: Dialog?,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
|
|
@ -41,12 +43,13 @@ fun BookInfoContent(
|
||||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||||
dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit,
|
dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit,
|
||||||
navigateToReader: () -> Unit,
|
navigateToReader: () -> Unit,
|
||||||
navigateToLibrary: () -> Unit,
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateBack: () -> Unit
|
navigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
BookInfoDialog(
|
BookInfoDialog(
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
book = book,
|
book = book,
|
||||||
|
categories = categories,
|
||||||
actionTitleDialog = actionTitleDialog,
|
actionTitleDialog = actionTitleDialog,
|
||||||
actionAuthorDialog = actionAuthorDialog,
|
actionAuthorDialog = actionAuthorDialog,
|
||||||
actionDescriptionDialog = actionDescriptionDialog,
|
actionDescriptionDialog = actionDescriptionDialog,
|
||||||
|
|
@ -54,8 +57,8 @@ fun BookInfoContent(
|
||||||
actionDeleteDialog = actionDeleteDialog,
|
actionDeleteDialog = actionDeleteDialog,
|
||||||
actionMoveDialog = actionMoveDialog,
|
actionMoveDialog = actionMoveDialog,
|
||||||
dismissDialog = dismissDialog,
|
dismissDialog = dismissDialog,
|
||||||
navigateToLibrary = navigateToLibrary,
|
navigateBack = navigateBack,
|
||||||
navigateBack = navigateBack
|
navigateToLibrarySettings = navigateToLibrarySettings
|
||||||
)
|
)
|
||||||
|
|
||||||
BookInfoBottomSheet(
|
BookInfoBottomSheet(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ package ua.acclorite.book_story.presentation.book_info
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.domain.util.Dialog
|
||||||
import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
||||||
import ua.acclorite.book_story.ui.book_info.BookInfoScreen
|
import ua.acclorite.book_story.ui.book_info.BookInfoScreen
|
||||||
|
|
@ -16,6 +17,7 @@ import ua.acclorite.book_story.ui.book_info.BookInfoScreen
|
||||||
fun BookInfoDialog(
|
fun BookInfoDialog(
|
||||||
dialog: Dialog?,
|
dialog: Dialog?,
|
||||||
book: Book,
|
book: Book,
|
||||||
|
categories: List<Category>,
|
||||||
actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit,
|
actionTitleDialog: (BookInfoEvent.OnActionTitleDialog) -> Unit,
|
||||||
actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit,
|
actionAuthorDialog: (BookInfoEvent.OnActionAuthorDialog) -> Unit,
|
||||||
actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit,
|
actionDescriptionDialog: (BookInfoEvent.OnActionDescriptionDialog) -> Unit,
|
||||||
|
|
@ -24,7 +26,7 @@ fun BookInfoDialog(
|
||||||
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
||||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
navigateToLibrary: () -> Unit
|
navigateToLibrarySettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
BookInfoScreen.DELETE_DIALOG -> {
|
BookInfoScreen.DELETE_DIALOG -> {
|
||||||
|
|
@ -38,9 +40,10 @@ fun BookInfoDialog(
|
||||||
BookInfoScreen.MOVE_DIALOG -> {
|
BookInfoScreen.MOVE_DIALOG -> {
|
||||||
BookInfoMoveDialog(
|
BookInfoMoveDialog(
|
||||||
book = book,
|
book = book,
|
||||||
|
categories = categories,
|
||||||
actionMoveDialog = actionMoveDialog,
|
actionMoveDialog = actionMoveDialog,
|
||||||
dismissDialog = dismissDialog,
|
dismissDialog = dismissDialog,
|
||||||
navigateToLibrary = navigateToLibrary
|
navigateToLibrarySettings = navigateToLibrarySettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,69 +6,93 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.presentation.book_info
|
package ua.acclorite.book_story.presentation.book_info
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
|
||||||
import androidx.compose.runtime.Composable
|
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.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
import ua.acclorite.book_story.domain.library.book.Book
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
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.Dialog
|
||||||
import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem
|
|
||||||
import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoMoveDialog(
|
fun BookInfoMoveDialog(
|
||||||
book: Book,
|
book: Book,
|
||||||
|
categories: List<Category>,
|
||||||
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (BookInfoEvent.OnActionMoveDialog) -> Unit,
|
||||||
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (BookInfoEvent.OnDismissDialog) -> Unit,
|
||||||
navigateToLibrary: () -> Unit
|
navigateToLibrarySettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val selectedCategories = remember {
|
||||||
val categories = remember {
|
mutableStateListOf<Category>().apply {
|
||||||
Category.entries.filter { book.category != it }
|
clear()
|
||||||
}
|
categories.forEach { category ->
|
||||||
val selectedCategory = remember {
|
if (book.categories.any { it == category.id }) {
|
||||||
mutableStateOf(categories[0])
|
add(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
title = stringResource(id = R.string.move_book),
|
title = stringResource(id = R.string.move),
|
||||||
icon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
description = null,
|
||||||
description = stringResource(
|
|
||||||
id = R.string.move_book_description
|
|
||||||
),
|
|
||||||
actionEnabled = true,
|
actionEnabled = true,
|
||||||
onDismiss = { dismissDialog(BookInfoEvent.OnDismissDialog) },
|
onDismiss = { dismissDialog(BookInfoEvent.OnDismissDialog) },
|
||||||
onAction = {
|
onAction = {
|
||||||
actionMoveDialog(
|
actionMoveDialog(
|
||||||
BookInfoEvent.OnActionMoveDialog(
|
BookInfoEvent.OnActionMoveDialog(
|
||||||
category = selectedCategory.value,
|
selectedCategories = selectedCategories,
|
||||||
context = context,
|
context = context
|
||||||
navigateToLibrary = navigateToLibrary
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
secondaryAction = stringResource(id = R.string.edit),
|
||||||
|
onSecondaryAction = {
|
||||||
|
navigateToLibrarySettings()
|
||||||
|
dismissDialog(BookInfoEvent.OnDismissDialog)
|
||||||
|
},
|
||||||
withContent = true,
|
withContent = true,
|
||||||
items = {
|
items = {
|
||||||
items(categories, key = { it.name }) {
|
items(categories) { category ->
|
||||||
val category = when (it) {
|
val selected = remember(
|
||||||
Category.READING -> stringResource(id = R.string.reading_tab)
|
category,
|
||||||
Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab)
|
selectedCategories
|
||||||
Category.PLANNING -> stringResource(id = R.string.planning_tab)
|
) {
|
||||||
Category.DROPPED -> stringResource(id = R.string.dropped_tab)
|
derivedStateOf {
|
||||||
|
selectedCategories.any { it == category }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectableDialogItem(
|
CheckboxDialogItem(
|
||||||
selected = it == selectedCategory.value,
|
selected = selected.value,
|
||||||
title = category
|
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) {
|
when (page) {
|
||||||
0 -> {
|
0 -> {
|
||||||
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
||||||
BrowseFilterSubcategory(
|
BrowseSortSubcategory(
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
showDivider = false
|
showDivider = false
|
||||||
)
|
)
|
||||||
|
|
@ -66,7 +66,7 @@ fun BrowseFilterBottomSheet(
|
||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) {
|
||||||
BrowseSortSubcategory(
|
BrowseFilterSubcategory(
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
showDivider = false
|
showDivider = false
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ fun BrowseFilterBottomSheetTabRow(
|
||||||
scrollToPage: (Int) -> Unit
|
scrollToPage: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val tabItems = listOf(
|
val tabItems = listOf(
|
||||||
stringResource(id = R.string.filter_tab),
|
|
||||||
stringResource(id = R.string.sort_tab),
|
stringResource(id = R.string.sort_tab),
|
||||||
|
stringResource(id = R.string.filter_tab),
|
||||||
stringResource(id = R.string.display_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 androidx.compose.ui.graphics.Color
|
||||||
import ua.acclorite.book_story.domain.library.book.Book
|
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.reader.ColorPreset
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
import ua.acclorite.book_story.domain.ui.UIText
|
||||||
|
|
||||||
|
|
@ -29,7 +28,7 @@ fun provideEmptyBook() = Book(
|
||||||
scrollOffset = 0,
|
scrollOffset = 0,
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.READING
|
categories = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Color Preset
|
// Default Color Preset
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,20 @@ object DataStoreConstants {
|
||||||
val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size")
|
val PROGRESS_BAR_FONT_SIZE = intPreferencesKey("progress_bar_font_size")
|
||||||
val PROGRESS_COUNT = stringPreferencesKey("progress_count")
|
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
|
// Browse settings
|
||||||
val BROWSE_LAYOUT = stringPreferencesKey("browse_layout")
|
val BROWSE_LAYOUT = stringPreferencesKey("browse_layout")
|
||||||
val BROWSE_AUTO_GRID_SIZE = booleanPreferencesKey("browse_auto_grid_size")
|
val BROWSE_AUTO_GRID_SIZE = booleanPreferencesKey("browse_auto_grid_size")
|
||||||
|
|
|
||||||
|
|
@ -66,3 +66,14 @@ fun Char.isVisibleCharacter(): Boolean {
|
||||||
else -> false
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
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.domain.util.Dialog
|
||||||
import ua.acclorite.book_story.ui.library.LibraryEvent
|
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)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -22,6 +29,11 @@ fun LibraryContent(
|
||||||
books: List<SelectableBook>,
|
books: List<SelectableBook>,
|
||||||
selectedItemsCount: Int,
|
selectedItemsCount: Int,
|
||||||
hasSelectedItems: Boolean,
|
hasSelectedItems: Boolean,
|
||||||
|
titlePosition: LibraryTitlePosition,
|
||||||
|
readButton: Boolean,
|
||||||
|
showProgress: Boolean,
|
||||||
|
showCategoryTabs: Boolean,
|
||||||
|
showBookCount: Boolean,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
bookCount: Int,
|
bookCount: Int,
|
||||||
|
|
@ -30,9 +42,21 @@ fun LibraryContent(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
doublePressExit: 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,
|
refreshState: PullRefreshState,
|
||||||
dialog: Dialog?,
|
dialog: Dialog?,
|
||||||
|
bottomSheet: BottomSheet?,
|
||||||
|
updateCategorySort: (SettingsEvent.OnUpdateCategorySort) -> Unit,
|
||||||
|
changeLibrarySortOrder: (MainEvent.OnChangeLibrarySortOrder) -> Unit,
|
||||||
|
changeLibrarySortOrderDescending: (MainEvent.OnChangeLibrarySortOrderDescending) -> Unit,
|
||||||
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
||||||
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
|
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
|
||||||
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
||||||
|
|
@ -44,9 +68,12 @@ fun LibraryContent(
|
||||||
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
||||||
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
|
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
|
dismissBottomSheet: (LibraryEvent.OnDismissBottomSheet) -> Unit,
|
||||||
navigateToBrowse: () -> Unit,
|
navigateToBrowse: () -> Unit,
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (id: Int) -> Unit,
|
||||||
navigateToReader: (id: Int) -> Unit
|
navigateToReader: (id: Int) -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
LibraryDialog(
|
LibraryDialog(
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
|
|
@ -55,12 +82,34 @@ fun LibraryContent(
|
||||||
selectedItemsCount = selectedItemsCount,
|
selectedItemsCount = selectedItemsCount,
|
||||||
actionMoveDialog = actionMoveDialog,
|
actionMoveDialog = actionMoveDialog,
|
||||||
actionDeleteDialog = actionDeleteDialog,
|
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(
|
LibraryScaffold(
|
||||||
|
books = books,
|
||||||
selectedItemsCount = selectedItemsCount,
|
selectedItemsCount = selectedItemsCount,
|
||||||
hasSelectedItems = hasSelectedItems,
|
hasSelectedItems = hasSelectedItems,
|
||||||
|
titlePosition = titlePosition,
|
||||||
|
readButton = readButton,
|
||||||
|
showProgress = showProgress,
|
||||||
|
showBookCount = showBookCount,
|
||||||
|
showCategoryTabs = showCategoryTabs,
|
||||||
showSearch = showSearch,
|
showSearch = showSearch,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
bookCount = bookCount,
|
bookCount = bookCount,
|
||||||
|
|
@ -68,7 +117,15 @@ fun LibraryContent(
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
|
layout = layout,
|
||||||
|
gridSize = gridSize,
|
||||||
|
autoGridSize = autoGridSize,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
showDefaultCategory = showDefaultCategory,
|
||||||
|
perCategorySort = perCategorySort,
|
||||||
|
categoriesSort = categoriesSort,
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
sortOrderDescending = sortOrderDescending,
|
||||||
searchVisibility = searchVisibility,
|
searchVisibility = searchVisibility,
|
||||||
requestFocus = requestFocus,
|
requestFocus = requestFocus,
|
||||||
searchQueryChange = searchQueryChange,
|
searchQueryChange = searchQueryChange,
|
||||||
|
|
@ -77,6 +134,7 @@ fun LibraryContent(
|
||||||
clearSelectedBooks = clearSelectedBooks,
|
clearSelectedBooks = clearSelectedBooks,
|
||||||
showMoveDialog = showMoveDialog,
|
showMoveDialog = showMoveDialog,
|
||||||
showDeleteDialog = showDeleteDialog,
|
showDeleteDialog = showDeleteDialog,
|
||||||
|
showFilterBottomSheet = showFilterBottomSheet,
|
||||||
refreshState = refreshState,
|
refreshState = refreshState,
|
||||||
navigateToBrowse = navigateToBrowse,
|
navigateToBrowse = navigateToBrowse,
|
||||||
navigateToBookInfo = navigateToBookInfo,
|
navigateToBookInfo = navigateToBookInfo,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package ua.acclorite.book_story.presentation.library
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
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.domain.util.Dialog
|
||||||
import ua.acclorite.book_story.ui.library.LibraryEvent
|
import ua.acclorite.book_story.ui.library.LibraryEvent
|
||||||
import ua.acclorite.book_story.ui.library.LibraryScreen
|
import ua.acclorite.book_story.ui.library.LibraryScreen
|
||||||
|
|
@ -17,20 +17,21 @@ import ua.acclorite.book_story.ui.library.LibraryScreen
|
||||||
fun LibraryDialog(
|
fun LibraryDialog(
|
||||||
dialog: Dialog?,
|
dialog: Dialog?,
|
||||||
books: List<SelectableBook>,
|
books: List<SelectableBook>,
|
||||||
categories: List<CategoryWithBooks>,
|
categories: List<Category>,
|
||||||
selectedItemsCount: Int,
|
selectedItemsCount: Int,
|
||||||
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
||||||
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
actionDeleteDialog: (LibraryEvent.OnActionDeleteDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
LibraryScreen.MOVE_DIALOG -> {
|
LibraryScreen.MOVE_DIALOG -> {
|
||||||
LibraryMoveDialog(
|
LibraryMoveDialog(
|
||||||
books = books,
|
books = books,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
selectedItemsCount = selectedItemsCount,
|
|
||||||
actionMoveDialog = actionMoveDialog,
|
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
|
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.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.domain.library.book.SelectableBook
|
||||||
import ua.acclorite.book_story.presentation.core.components.common.AsyncCoverImage
|
import ua.acclorite.book_story.domain.library.display.LibraryLayout
|
||||||
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
import ua.acclorite.book_story.domain.library.display.LibraryTitlePosition
|
||||||
import ua.acclorite.book_story.presentation.core.util.calculateProgress
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyGridItemScope.LibraryItem(
|
fun LibraryItem(
|
||||||
book: SelectableBook,
|
book: SelectableBook,
|
||||||
|
layout: LibraryLayout,
|
||||||
hasSelectedItems: Boolean,
|
hasSelectedItems: Boolean,
|
||||||
|
titlePosition: LibraryTitlePosition,
|
||||||
|
readButton: Boolean,
|
||||||
|
showProgress: Boolean,
|
||||||
selectBook: (select: Boolean?) -> Unit,
|
selectBook: (select: Boolean?) -> Unit,
|
||||||
navigateToBookInfo: () -> Unit,
|
navigateToBookInfo: () -> Unit,
|
||||||
navigateToReader: () -> Unit
|
navigateToReader: () -> Unit
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (book.selected) MaterialTheme.colorScheme.secondary
|
when (layout) {
|
||||||
else Color.Transparent
|
LibraryLayout.LIST -> {
|
||||||
val fontColor = if (book.selected) MaterialTheme.colorScheme.onSecondary
|
LibraryListItem(
|
||||||
else MaterialTheme.colorScheme.onSurface
|
book = book,
|
||||||
|
hasSelectedItems = hasSelectedItems,
|
||||||
val progress = rememberSaveable(book.data.progress) {
|
readButton = readButton,
|
||||||
"${book.data.progress.calculateProgress(1)}%"
|
showProgress = showProgress,
|
||||||
}
|
selectBook = selectBook,
|
||||||
|
navigateToBookInfo = navigateToBookInfo,
|
||||||
Column(
|
navigateToReader = navigateToReader
|
||||||
Modifier
|
)
|
||||||
.animateItem()
|
}
|
||||||
.padding(3.dp)
|
|
||||||
.background(
|
LibraryLayout.GRID -> {
|
||||||
backgroundColor,
|
LibraryGridItem(
|
||||||
MaterialTheme.shapes.large
|
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
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
||||||
import androidx.compose.ui.unit.dp
|
import ua.acclorite.book_story.domain.library.display.LibraryLayout
|
||||||
import ua.acclorite.book_story.presentation.core.components.common.LazyVerticalGridWithScrollbar
|
|
||||||
import ua.acclorite.book_story.presentation.core.constants.providePrimaryScrollbar
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryLayout(
|
fun LibraryLayout(
|
||||||
items: LazyGridScope.() -> Unit
|
books: List<SelectableBook>,
|
||||||
|
layout: LibraryLayout,
|
||||||
|
gridSize: Int,
|
||||||
|
autoGridSize: Boolean,
|
||||||
|
itemContent: @Composable (book: SelectableBook) -> Unit
|
||||||
) {
|
) {
|
||||||
LazyVerticalGridWithScrollbar(
|
when (layout) {
|
||||||
columns = GridCells.Adaptive(120.dp),
|
LibraryLayout.LIST -> {
|
||||||
modifier = Modifier.fillMaxSize(),
|
LibraryListLayout(
|
||||||
scrollbarSettings = providePrimaryScrollbar(false),
|
books = books,
|
||||||
contentPadding = PaddingValues(8.dp)
|
itemContent = itemContent
|
||||||
) {
|
)
|
||||||
items()
|
}
|
||||||
|
|
||||||
|
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
|
package ua.acclorite.book_story.presentation.library
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.icons.outlined.MoveUp
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
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.Dialog
|
||||||
import ua.acclorite.book_story.presentation.core.components.dialog.SelectableDialogItem
|
|
||||||
import ua.acclorite.book_story.ui.library.LibraryEvent
|
import ua.acclorite.book_story.ui.library.LibraryEvent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryMoveDialog(
|
fun LibraryMoveDialog(
|
||||||
books: List<SelectableBook>,
|
books: List<SelectableBook>,
|
||||||
categories: List<CategoryWithBooks>,
|
categories: List<Category>,
|
||||||
selectedItemsCount: Int,
|
|
||||||
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
actionMoveDialog: (LibraryEvent.OnActionMoveDialog) -> Unit,
|
||||||
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit
|
dismissDialog: (LibraryEvent.OnDismissDialog) -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val selectedBooks = remember {
|
val selectedBooks = remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
books.filter { it.selected }
|
books.filter { it.selected }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val moveCategories = remember {
|
val selectedCategories = remember {
|
||||||
derivedStateOf {
|
mutableStateListOf<Category>().apply {
|
||||||
categories.mapNotNull { category ->
|
clear()
|
||||||
if (!selectedBooks.value.all { it.data.category == category.category }) {
|
categories.forEach { category ->
|
||||||
return@mapNotNull 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(
|
Dialog(
|
||||||
title = stringResource(id = R.string.move_books),
|
title = stringResource(id = R.string.move),
|
||||||
icon = Icons.Outlined.MoveUp,
|
description = null,
|
||||||
description = stringResource(
|
|
||||||
id = R.string.move_books_description,
|
|
||||||
selectedItemsCount
|
|
||||||
),
|
|
||||||
actionEnabled = true,
|
actionEnabled = true,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
dismissDialog(LibraryEvent.OnDismissDialog)
|
dismissDialog(LibraryEvent.OnDismissDialog)
|
||||||
|
|
@ -64,20 +61,46 @@ fun LibraryMoveDialog(
|
||||||
onAction = {
|
onAction = {
|
||||||
actionMoveDialog(
|
actionMoveDialog(
|
||||||
LibraryEvent.OnActionMoveDialog(
|
LibraryEvent.OnActionMoveDialog(
|
||||||
selectedCategory = selectedCategory.value.category,
|
selectedCategories = selectedCategories,
|
||||||
categories = categories,
|
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
secondaryAction = stringResource(id = R.string.edit),
|
||||||
|
onSecondaryAction = {
|
||||||
|
navigateToLibrarySettings()
|
||||||
|
dismissDialog(LibraryEvent.OnDismissDialog)
|
||||||
|
},
|
||||||
withContent = true,
|
withContent = true,
|
||||||
items = {
|
items = {
|
||||||
items(moveCategories.value) { category ->
|
items(categories) { category ->
|
||||||
SelectableDialogItem(
|
val selected = remember(
|
||||||
selected = category == selectedCategory.value,
|
category,
|
||||||
title = category.title.asString()
|
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.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.library.LibraryEvent
|
||||||
import ua.acclorite.book_story.ui.theme.DefaultTransition
|
import ua.acclorite.book_story.ui.theme.DefaultTransition
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryPager(
|
fun LibraryPager(
|
||||||
|
books: List<SelectableBook>,
|
||||||
pagerState: PagerState,
|
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,
|
hasSelectedItems: Boolean,
|
||||||
|
titlePosition: LibraryTitlePosition,
|
||||||
|
readButton: Boolean,
|
||||||
|
showProgress: Boolean,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
||||||
|
|
@ -31,42 +48,140 @@ fun LibraryPager(
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (id: Int) -> Unit,
|
||||||
navigateToReader: (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 ->
|
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { index ->
|
||||||
val category = remember(categories, index) {
|
val category = remember(categorizedBooks, index) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
categories[index]
|
categorizedBooks.value[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
DefaultTransition(visible = !isLoading) {
|
DefaultTransition(visible = !isLoading) {
|
||||||
LibraryLayout {
|
LibraryLayout(
|
||||||
items(
|
books = category.value,
|
||||||
category.value.books,
|
gridSize = gridSize,
|
||||||
key = { it.data.id }
|
autoGridSize = autoGridSize,
|
||||||
) { book ->
|
layout = layout
|
||||||
LibraryItem(
|
) { book ->
|
||||||
book = book,
|
LibraryItem(
|
||||||
hasSelectedItems = hasSelectedItems,
|
book = book,
|
||||||
selectBook = { select ->
|
layout = layout,
|
||||||
selectBook(
|
hasSelectedItems = hasSelectedItems,
|
||||||
LibraryEvent.OnSelectBook(
|
titlePosition = titlePosition,
|
||||||
id = book.data.id,
|
readButton = readButton,
|
||||||
select = select
|
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(
|
LibraryEmptyPlaceholder(
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
isBooksEmpty = category.value.books.isEmpty(),
|
isBooksEmpty = category.value.isEmpty(),
|
||||||
navigateToBrowse = navigateToBrowse
|
navigateToBrowse = navigateToBrowse
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,25 @@ import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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
|
import ua.acclorite.book_story.ui.library.LibraryEvent
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScaffold(
|
fun LibraryScaffold(
|
||||||
|
books: List<SelectableBook>,
|
||||||
selectedItemsCount: Int,
|
selectedItemsCount: Int,
|
||||||
hasSelectedItems: Boolean,
|
hasSelectedItems: Boolean,
|
||||||
|
titlePosition: LibraryTitlePosition,
|
||||||
|
readButton: Boolean,
|
||||||
|
showProgress: Boolean,
|
||||||
|
showCategoryTabs: Boolean,
|
||||||
|
showBookCount: Boolean,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
bookCount: Int,
|
bookCount: Int,
|
||||||
|
|
@ -34,7 +45,15 @@ fun LibraryScaffold(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
refreshState: PullRefreshState,
|
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,
|
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
|
||||||
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
selectBook: (LibraryEvent.OnSelectBook) -> Unit,
|
||||||
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
||||||
|
|
@ -43,6 +62,7 @@ fun LibraryScaffold(
|
||||||
clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit,
|
clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit,
|
||||||
showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit,
|
showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> Unit,
|
||||||
showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit,
|
showDeleteDialog: (LibraryEvent.OnShowDeleteDialog) -> Unit,
|
||||||
|
showFilterBottomSheet: (LibraryEvent.OnShowFilterBottomSheet) -> Unit,
|
||||||
navigateToBrowse: () -> Unit,
|
navigateToBrowse: () -> Unit,
|
||||||
navigateToBookInfo: (id: Int) -> Unit,
|
navigateToBookInfo: (id: Int) -> Unit,
|
||||||
navigateToReader: (id: Int) -> Unit,
|
navigateToReader: (id: Int) -> Unit,
|
||||||
|
|
@ -54,8 +74,11 @@ fun LibraryScaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
LibraryTopBar(
|
LibraryTopBar(
|
||||||
|
books = books,
|
||||||
selectedItemsCount = selectedItemsCount,
|
selectedItemsCount = selectedItemsCount,
|
||||||
hasSelectedItems = hasSelectedItems,
|
hasSelectedItems = hasSelectedItems,
|
||||||
|
showBookCount = showBookCount,
|
||||||
|
showCategoryTabs = showCategoryTabs,
|
||||||
showSearch = showSearch,
|
showSearch = showSearch,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
bookCount = bookCount,
|
bookCount = bookCount,
|
||||||
|
|
@ -64,13 +87,15 @@ fun LibraryScaffold(
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
showDefaultCategory = showDefaultCategory,
|
||||||
searchVisibility = searchVisibility,
|
searchVisibility = searchVisibility,
|
||||||
requestFocus = requestFocus,
|
requestFocus = requestFocus,
|
||||||
searchQueryChange = searchQueryChange,
|
searchQueryChange = searchQueryChange,
|
||||||
search = search,
|
search = search,
|
||||||
clearSelectedBooks = clearSelectedBooks,
|
clearSelectedBooks = clearSelectedBooks,
|
||||||
showMoveDialog = showMoveDialog,
|
showMoveDialog = showMoveDialog,
|
||||||
showDeleteDialog = showDeleteDialog
|
showDeleteDialog = showDeleteDialog,
|
||||||
|
showFilterBottomSheet = showFilterBottomSheet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|
@ -80,15 +105,27 @@ fun LibraryScaffold(
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
.padding(top = paddingValues.calculateTopPadding())
|
||||||
) {
|
) {
|
||||||
LibraryPager(
|
LibraryPager(
|
||||||
|
books = books,
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
showDefaultCategory = showDefaultCategory,
|
||||||
|
perCategorySort = perCategorySort,
|
||||||
|
categoriesSort = categoriesSort,
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
sortOrderDescending = sortOrderDescending,
|
||||||
|
layout = layout,
|
||||||
|
gridSize = gridSize,
|
||||||
|
autoGridSize = autoGridSize,
|
||||||
hasSelectedItems = hasSelectedItems,
|
hasSelectedItems = hasSelectedItems,
|
||||||
|
titlePosition = titlePosition,
|
||||||
|
readButton = readButton,
|
||||||
|
showProgress = showProgress,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
selectBook = selectBook,
|
selectBook = selectBook,
|
||||||
navigateToBrowse = navigateToBrowse,
|
navigateToBrowse = navigateToBrowse,
|
||||||
navigateToReader = navigateToReader,
|
navigateToReader = navigateToReader,
|
||||||
navigateToBookInfo = navigateToBookInfo,
|
navigateToBookInfo = navigateToBookInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
LibraryRefreshIndicator(
|
LibraryRefreshIndicator(
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,15 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
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
|
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryTabs(
|
fun LibraryTabs(
|
||||||
categories: List<CategoryWithBooks>,
|
categoriesWithBookCount: List<Pair<Category, Int>>,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
itemCountBackgroundColor: Color,
|
itemCountBackgroundColor: Color,
|
||||||
|
showBookCount: Boolean
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
@ -69,7 +70,7 @@ fun LibraryTabs(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
categories.forEachIndexed { index, tabItem ->
|
categoriesWithBookCount.forEachIndexed { index, tabItem ->
|
||||||
Tab(
|
Tab(
|
||||||
selected = pagerState.currentPage == index,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -82,23 +83,26 @@ fun LibraryTabs(
|
||||||
text = {
|
text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
StyledText(
|
StyledText(
|
||||||
text = tabItem.title.asString(),
|
text = tabItem.first.title,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
StyledText(
|
if (showBookCount) {
|
||||||
text = tabItem.books.count().toString(),
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
modifier = Modifier
|
StyledText(
|
||||||
.background(
|
text = tabItem.second.toString(),
|
||||||
itemCountBackgroundColor,
|
modifier = Modifier
|
||||||
MaterialTheme.shapes.medium
|
.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.animateColorAsState
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
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.background
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Clear
|
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.filled.Search
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.MoveUp
|
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.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ua.acclorite.book_story.R
|
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.IconButton
|
||||||
import ua.acclorite.book_story.presentation.core.components.common.SearchTextField
|
import ua.acclorite.book_story.presentation.core.components.common.SearchTextField
|
||||||
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryTopBar(
|
fun LibraryTopBar(
|
||||||
|
books: List<SelectableBook>,
|
||||||
selectedItemsCount: Int,
|
selectedItemsCount: Int,
|
||||||
hasSelectedItems: Boolean,
|
hasSelectedItems: Boolean,
|
||||||
|
showBookCount: Boolean,
|
||||||
|
showCategoryTabs: Boolean,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
bookCount: Int,
|
bookCount: Int,
|
||||||
|
|
@ -56,15 +66,49 @@ fun LibraryTopBar(
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
isRefreshing: Boolean,
|
isRefreshing: Boolean,
|
||||||
categories: List<CategoryWithBooks>,
|
categories: List<Category>,
|
||||||
|
showDefaultCategory: Boolean,
|
||||||
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
|
searchVisibility: (LibraryEvent.OnSearchVisibility) -> Unit,
|
||||||
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
requestFocus: (LibraryEvent.OnRequestFocus) -> Unit,
|
||||||
searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit,
|
searchQueryChange: (LibraryEvent.OnSearchQueryChange) -> Unit,
|
||||||
search: (LibraryEvent.OnSearch) -> Unit,
|
search: (LibraryEvent.OnSearch) -> Unit,
|
||||||
clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit,
|
clearSelectedBooks: (LibraryEvent.OnClearSelectedBooks) -> Unit,
|
||||||
showMoveDialog: (LibraryEvent.OnShowMoveDialog) -> 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(
|
val animatedItemCountBackgroundColor = animateColorAsState(
|
||||||
if (hasSelectedItems) MaterialTheme.colorScheme.surfaceContainerHighest
|
if (hasSelectedItems) MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
else MaterialTheme.colorScheme.surfaceContainer,
|
else MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
|
@ -86,21 +130,28 @@ fun LibraryTopBar(
|
||||||
contentNavigationIcon = {},
|
contentNavigationIcon = {},
|
||||||
contentTitle = {
|
contentTitle = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
StyledText(text = stringResource(id = R.string.library_screen))
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
StyledText(
|
StyledText(
|
||||||
text = bookCount.toString(),
|
text = if (showCategoryTabs) stringResource(id = R.string.library_screen)
|
||||||
modifier = Modifier
|
else currentCategory.value.first.title
|
||||||
.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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 = {
|
contentActions = {
|
||||||
|
|
@ -111,6 +162,13 @@ fun LibraryTopBar(
|
||||||
) {
|
) {
|
||||||
searchVisibility(LibraryEvent.OnSearchVisibility(true))
|
searchVisibility(LibraryEvent.OnSearchVisibility(true))
|
||||||
}
|
}
|
||||||
|
IconButton(
|
||||||
|
icon = Icons.Default.FilterList,
|
||||||
|
contentDescription = R.string.filter_content_desc,
|
||||||
|
disableOnClick = false,
|
||||||
|
) {
|
||||||
|
showFilterBottomSheet(LibraryEvent.OnShowFilterBottomSheet)
|
||||||
|
}
|
||||||
NavigatorIconButton()
|
NavigatorIconButton()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
@ -188,11 +246,18 @@ fun LibraryTopBar(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
customContent = {
|
customContent = {
|
||||||
LibraryTabs(
|
AnimatedVisibility(
|
||||||
categories = categories,
|
visible = showCategoryTabs,
|
||||||
pagerState = pagerState,
|
enter = expandVertically(),
|
||||||
itemCountBackgroundColor = animatedItemCountBackgroundColor.value
|
exit = shrinkVertically()
|
||||||
)
|
) {
|
||||||
|
LibraryTabs(
|
||||||
|
categoriesWithBookCount = categoriesWithBookCount.value,
|
||||||
|
pagerState = pagerState,
|
||||||
|
itemCountBackgroundColor = animatedItemCountBackgroundColor.value,
|
||||||
|
showBookCount = showBookCount
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ fun SettingsContent(
|
||||||
navigateToGeneralSettings: () -> Unit,
|
navigateToGeneralSettings: () -> Unit,
|
||||||
navigateToAppearanceSettings: () -> Unit,
|
navigateToAppearanceSettings: () -> Unit,
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: () -> Unit,
|
||||||
navigateBack: () -> Unit
|
navigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -28,6 +29,7 @@ fun SettingsContent(
|
||||||
navigateToGeneralSettings = navigateToGeneralSettings,
|
navigateToGeneralSettings = navigateToGeneralSettings,
|
||||||
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
||||||
navigateToReaderSettings = navigateToReaderSettings,
|
navigateToReaderSettings = navigateToReaderSettings,
|
||||||
|
navigateToLibrarySettings = navigateToLibrarySettings,
|
||||||
navigateToBrowseSettings = navigateToBrowseSettings,
|
navigateToBrowseSettings = navigateToBrowseSettings,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
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.DisplaySettings
|
||||||
import androidx.compose.material.icons.outlined.Explore
|
import androidx.compose.material.icons.outlined.Explore
|
||||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||||
|
|
@ -29,6 +30,7 @@ fun SettingsLayout(
|
||||||
navigateToGeneralSettings: () -> Unit,
|
navigateToGeneralSettings: () -> Unit,
|
||||||
navigateToAppearanceSettings: () -> Unit,
|
navigateToAppearanceSettings: () -> Unit,
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit
|
navigateToBrowseSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(
|
||||||
|
|
@ -74,6 +76,17 @@ fun SettingsLayout(
|
||||||
item {
|
item {
|
||||||
SettingsLayoutItem(
|
SettingsLayoutItem(
|
||||||
index = 3,
|
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,
|
icon = Icons.Outlined.Explore,
|
||||||
title = stringResource(id = R.string.browse_settings),
|
title = stringResource(id = R.string.browse_settings),
|
||||||
description = stringResource(id = R.string.browse_settings_desc)
|
description = stringResource(id = R.string.browse_settings_desc)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ fun SettingsScaffold(
|
||||||
navigateToGeneralSettings: () -> Unit,
|
navigateToGeneralSettings: () -> Unit,
|
||||||
navigateToAppearanceSettings: () -> Unit,
|
navigateToAppearanceSettings: () -> Unit,
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: () -> Unit,
|
||||||
navigateBack: () -> Unit
|
navigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -49,6 +50,7 @@ fun SettingsScaffold(
|
||||||
navigateToGeneralSettings = navigateToGeneralSettings,
|
navigateToGeneralSettings = navigateToGeneralSettings,
|
||||||
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
||||||
navigateToReaderSettings = navigateToReaderSettings,
|
navigateToReaderSettings = navigateToReaderSettings,
|
||||||
|
navigateToLibrarySettings = navigateToLibrarySettings,
|
||||||
navigateToBrowseSettings = navigateToBrowseSettings
|
navigateToBrowseSettings = navigateToBrowseSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,11 @@ package ua.acclorite.book_story.presentation.settings.browse
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import ua.acclorite.book_story.presentation.settings.browse.display.BrowseDisplaySubcategory
|
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.scan.BrowseScanSubcategory
|
||||||
import ua.acclorite.book_story.presentation.settings.browse.sort.BrowseSortSubcategory
|
|
||||||
|
|
||||||
fun LazyListScope.BrowseSettingsCategory() {
|
fun LazyListScope.BrowseSettingsCategory() {
|
||||||
BrowseScanSubcategory()
|
BrowseScanSubcategory()
|
||||||
BrowseDisplaySubcategory()
|
BrowseDisplaySubcategory(
|
||||||
BrowseFilterSubcategory()
|
|
||||||
BrowseSortSubcategory(
|
|
||||||
showDivider = false
|
showDivider = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -25,12 +25,12 @@ fun BrowseGridSizeOption() {
|
||||||
ExpandingTransition(visible = state.value.browseLayout == BrowseLayout.GRID) {
|
ExpandingTransition(visible = state.value.browseLayout == BrowseLayout.GRID) {
|
||||||
SliderWithTitle(
|
SliderWithTitle(
|
||||||
value = state.value.browseGridSize
|
value = state.value.browseGridSize
|
||||||
to " ${stringResource(R.string.browse_grid_size_per_row)}",
|
to " ${stringResource(R.string.grid_size_per_row)}",
|
||||||
valuePlaceholder = stringResource(id = R.string.browse_grid_size_auto),
|
valuePlaceholder = stringResource(id = R.string.grid_size_auto),
|
||||||
showPlaceholder = state.value.browseAutoGridSize,
|
showPlaceholder = state.value.browseAutoGridSize,
|
||||||
fromValue = 0,
|
fromValue = 0,
|
||||||
toValue = 10,
|
toValue = 10,
|
||||||
title = stringResource(id = R.string.browse_grid_size_option),
|
title = stringResource(id = R.string.grid_size_option),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
mainModel.onEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0))
|
mainModel.onEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0))
|
||||||
mainModel.onEvent(
|
mainModel.onEvent(
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import ua.acclorite.book_story.presentation.settings.components.SettingsSubcateg
|
||||||
|
|
||||||
fun LazyListScope.BrowseSortSubcategory(
|
fun LazyListScope.BrowseSortSubcategory(
|
||||||
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
|
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,
|
showTitle: Boolean = true,
|
||||||
showDivider: Boolean = true
|
showDivider: Boolean = true
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,10 @@ private fun BrowseSortOptionItem(
|
||||||
StyledText(
|
StyledText(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
when (item) {
|
when (item) {
|
||||||
BrowseSortOrder.NAME -> R.string.browse_sort_order_name
|
BrowseSortOrder.NAME -> R.string.sort_order_name
|
||||||
BrowseSortOrder.FILE_FORMAT -> R.string.browse_sort_order_file_format
|
BrowseSortOrder.FILE_FORMAT -> R.string.sort_order_file_format
|
||||||
BrowseSortOrder.LAST_MODIFIED -> R.string.browse_sort_order_last_modified
|
BrowseSortOrder.LAST_MODIFIED -> R.string.sort_order_last_modified
|
||||||
BrowseSortOrder.FILE_SIZE -> R.string.browse_sort_order_file_size
|
BrowseSortOrder.FILE_SIZE -> R.string.sort_order_file_size
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
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 object OnShowMoveDialog : BookInfoEvent()
|
||||||
|
|
||||||
data class OnActionMoveDialog(
|
data class OnActionMoveDialog(
|
||||||
val category: Category,
|
val selectedCategories: List<Category>,
|
||||||
val context: Context,
|
val context: Context
|
||||||
val navigateToLibrary: () -> Unit
|
|
||||||
) : BookInfoEvent()
|
) : BookInfoEvent()
|
||||||
|
|
||||||
data object OnDismissDialog : BookInfoEvent()
|
data object OnDismissDialog : BookInfoEvent()
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import ua.acclorite.book_story.R
|
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.CanResetCover
|
||||||
import ua.acclorite.book_story.domain.use_case.book.DeleteBooks
|
import ua.acclorite.book_story.domain.use_case.book.DeleteBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.book.GetBookById
|
import ua.acclorite.book_story.domain.use_case.book.GetBookById
|
||||||
|
|
@ -357,34 +356,22 @@ class BookInfoModel @Inject constructor(
|
||||||
launch {
|
launch {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
book = it.book.copy(
|
||||||
|
categories = event.selectedCategories.map { it.id }
|
||||||
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
bottomSheet = null
|
bottomSheet = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
book = it.book.copy(
|
|
||||||
category = event.category
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
updateBook.execute(_state.value.book)
|
updateBook.execute(_state.value.book)
|
||||||
|
|
||||||
LibraryScreen.refreshListChannel.trySend(0)
|
LibraryScreen.refreshListChannel.trySend(0)
|
||||||
LibraryScreen.scrollToPageCompositionChannel.trySend(
|
|
||||||
Category.entries.dropLastWhile {
|
|
||||||
it != event.category
|
|
||||||
}.size - 1
|
|
||||||
)
|
|
||||||
HistoryScreen.refreshListChannel.trySend(0)
|
HistoryScreen.refreshListChannel.trySend(0)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
event.context.getString(R.string.book_moved)
|
event.context.getString(R.string.book_moved)
|
||||||
.showToast(context = event.context)
|
.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.book_info.BookInfoContent
|
||||||
import ua.acclorite.book_story.presentation.navigator.LocalNavigator
|
import ua.acclorite.book_story.presentation.navigator.LocalNavigator
|
||||||
import ua.acclorite.book_story.ui.history.HistoryScreen
|
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.reader.ReaderScreen
|
||||||
|
import ua.acclorite.book_story.ui.settings.LibrarySettingsScreen
|
||||||
|
import ua.acclorite.book_story.ui.settings.SettingsModel
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
||||||
|
|
@ -47,7 +48,9 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val screenModel = hiltViewModel<BookInfoModel>()
|
val screenModel = hiltViewModel<BookInfoModel>()
|
||||||
|
val settingsModel = hiltViewModel<SettingsModel>()
|
||||||
|
|
||||||
|
val settingsState = settingsModel.state.collectAsStateWithLifecycle()
|
||||||
val state = screenModel.state.collectAsStateWithLifecycle()
|
val state = screenModel.state.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
|
@ -72,6 +75,7 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
||||||
if (state.value.book.id == bookId) {
|
if (state.value.book.id == bookId) {
|
||||||
BookInfoContent(
|
BookInfoContent(
|
||||||
book = state.value.book,
|
book = state.value.book,
|
||||||
|
categories = settingsState.value.categories,
|
||||||
bottomSheet = state.value.bottomSheet,
|
bottomSheet = state.value.bottomSheet,
|
||||||
dialog = state.value.dialog,
|
dialog = state.value.dialog,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
|
|
@ -102,12 +106,8 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
||||||
navigator.push(ReaderScreen(state.value.book.id))
|
navigator.push(ReaderScreen(state.value.book.id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigateToLibrary = {
|
navigateToLibrarySettings = {
|
||||||
navigator.push(
|
navigator.push(LibrarySettingsScreen)
|
||||||
LibraryScreen,
|
|
||||||
popping = true,
|
|
||||||
saveInBackStack = false
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
navigateBack = {
|
navigateBack = {
|
||||||
navigator.pop()
|
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.book.InsertBook
|
||||||
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile
|
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.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.presentation.core.util.showToast
|
||||||
import ua.acclorite.book_story.ui.library.LibraryScreen
|
import ua.acclorite.book_story.ui.library.LibraryScreen
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -419,16 +420,6 @@ class BrowseModel @Inject constructor(
|
||||||
includedFilterItems: List<String>,
|
includedFilterItems: List<String>,
|
||||||
sortOrder: BrowseSortOrder
|
sortOrder: BrowseSortOrder
|
||||||
): List<SelectableFile> {
|
): List<SelectableFile> {
|
||||||
fun <T> compareByWithOrder(
|
|
||||||
selector: (T) -> Comparable<*>?
|
|
||||||
): Comparator<T> {
|
|
||||||
return if (sortOrderDescending) {
|
|
||||||
compareByDescending(selector)
|
|
||||||
} else {
|
|
||||||
compareBy(selector)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<SelectableFile>.filterFiles(): List<SelectableFile> {
|
fun List<SelectableFile>.filterFiles(): List<SelectableFile> {
|
||||||
if (includedFilterItems.isEmpty()) {
|
if (includedFilterItems.isEmpty()) {
|
||||||
return this
|
return this
|
||||||
|
|
@ -446,7 +437,7 @@ class BrowseModel @Inject constructor(
|
||||||
return files
|
return files
|
||||||
.filterFiles()
|
.filterFiles()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareByWithOrder {
|
compareByWithOrder(sortOrderDescending) {
|
||||||
when (sortOrder) {
|
when (sortOrder) {
|
||||||
BrowseSortOrder.NAME -> {
|
BrowseSortOrder.NAME -> {
|
||||||
it.data.name.trim()
|
it.data.name.trim()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import android.content.Context
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import ua.acclorite.book_story.domain.library.category.Category
|
import ua.acclorite.book_story.domain.library.category.Category
|
||||||
import ua.acclorite.book_story.domain.library.category.CategoryWithBooks
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class LibraryEvent {
|
sealed class LibraryEvent {
|
||||||
|
|
@ -43,8 +42,7 @@ sealed class LibraryEvent {
|
||||||
data object OnShowMoveDialog : LibraryEvent()
|
data object OnShowMoveDialog : LibraryEvent()
|
||||||
|
|
||||||
data class OnActionMoveDialog(
|
data class OnActionMoveDialog(
|
||||||
val selectedCategory: Category,
|
val selectedCategories: List<Category>,
|
||||||
val categories: List<CategoryWithBooks>,
|
|
||||||
val context: Context
|
val context: Context
|
||||||
) : LibraryEvent()
|
) : LibraryEvent()
|
||||||
|
|
||||||
|
|
@ -55,4 +53,8 @@ sealed class LibraryEvent {
|
||||||
) : LibraryEvent()
|
) : LibraryEvent()
|
||||||
|
|
||||||
data object OnDismissDialog : 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
|
if (!book.selected) return@forEach
|
||||||
moveBooks.execute(
|
moveBooks.execute(
|
||||||
book.data.copy(
|
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
|
if (!book.selected) return@map book
|
||||||
book.copy(
|
book.copy(
|
||||||
data = book.data.copy(
|
data = book.data.copy(
|
||||||
category = event.selectedCategory
|
categories = event.selectedCategories.map { it.id }
|
||||||
),
|
),
|
||||||
selected = false
|
selected = false
|
||||||
)
|
)
|
||||||
|
|
@ -232,11 +232,6 @@ class LibraryModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryScreen.refreshListChannel.trySend(0)
|
HistoryScreen.refreshListChannel.trySend(0)
|
||||||
LibraryScreen.scrollToPageCompositionChannel.trySend(
|
|
||||||
event.categories.dropLastWhile {
|
|
||||||
it.category != event.selectedCategory
|
|
||||||
}.lastIndex
|
|
||||||
)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
event.context
|
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.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
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.navigator.Screen
|
||||||
import ua.acclorite.book_story.domain.ui.UIText
|
|
||||||
import ua.acclorite.book_story.presentation.library.LibraryContent
|
import ua.acclorite.book_story.presentation.library.LibraryContent
|
||||||
import ua.acclorite.book_story.presentation.navigator.LocalNavigator
|
import ua.acclorite.book_story.presentation.navigator.LocalNavigator
|
||||||
import ua.acclorite.book_story.ui.book_info.BookInfoScreen
|
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.history.HistoryScreen
|
||||||
import ua.acclorite.book_story.ui.main.MainModel
|
import ua.acclorite.book_story.ui.main.MainModel
|
||||||
import ua.acclorite.book_story.ui.reader.ReaderScreen
|
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
|
@Parcelize
|
||||||
object LibraryScreen : Screen, Parcelable {
|
object LibraryScreen : Screen, Parcelable {
|
||||||
|
|
@ -45,6 +43,9 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
const val DELETE_DIALOG = "delete_dialog"
|
const val DELETE_DIALOG = "delete_dialog"
|
||||||
|
|
||||||
|
@IgnoredOnParcel
|
||||||
|
const val FILTER_BOTTOM_SHEET = "filter_bottom_sheet"
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val refreshListChannel: Channel<Long> = Channel(Channel.CONFLATED)
|
val refreshListChannel: Channel<Long> = Channel(Channel.CONFLATED)
|
||||||
|
|
||||||
|
|
@ -61,34 +62,19 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
|
|
||||||
val screenModel = hiltViewModel<LibraryModel>()
|
val screenModel = hiltViewModel<LibraryModel>()
|
||||||
val mainModel = hiltViewModel<MainModel>()
|
val mainModel = hiltViewModel<MainModel>()
|
||||||
|
val settingsModel = hiltViewModel<SettingsModel>()
|
||||||
|
|
||||||
val state = screenModel.state.collectAsStateWithLifecycle()
|
val state = screenModel.state.collectAsStateWithLifecycle()
|
||||||
val mainState = mainModel.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 {
|
derivedStateOf {
|
||||||
listOf(
|
val categoryIds = settingsState.value.categories.map { it.id }.toSet()
|
||||||
CategoryWithBooks(
|
state.value.books.any { book ->
|
||||||
category = Category.READING,
|
book.data.categories.none { category -> category in categoryIds }
|
||||||
title = UIText.StringResource(R.string.reading_tab),
|
} || settingsState.value.categories.isEmpty()
|
||||||
books = state.value.books.filter { it.data.category == Category.READING }
|
|| mainState.value.libraryAlwaysShowDefaultTab
|
||||||
),
|
|
||||||
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 }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,12 +93,14 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
|
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = initialPage
|
initialPage = initialPage
|
||||||
) { categories.value.count() }
|
) {
|
||||||
|
settingsState.value.categories.count().plus(if (showDefaultCategory.value) 1 else 0)
|
||||||
|
}
|
||||||
DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } }
|
DisposableEffect(Unit) { onDispose { initialPage = pagerState.currentPage } }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
scrollToPageCompositionChannel.receiveAsFlow().collectLatest {
|
scrollToPageCompositionChannel.receiveAsFlow().collectLatest {
|
||||||
pagerState.animateScrollToPage(it)
|
pagerState.scrollToPage(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +108,11 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
books = state.value.books,
|
books = state.value.books,
|
||||||
selectedItemsCount = state.value.selectedItemsCount,
|
selectedItemsCount = state.value.selectedItemsCount,
|
||||||
hasSelectedItems = state.value.hasSelectedItems,
|
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,
|
showSearch = state.value.showSearch,
|
||||||
searchQuery = state.value.searchQuery,
|
searchQuery = state.value.searchQuery,
|
||||||
bookCount = state.value.books.count(),
|
bookCount = state.value.books.count(),
|
||||||
|
|
@ -128,9 +121,21 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
isLoading = state.value.isLoading,
|
isLoading = state.value.isLoading,
|
||||||
isRefreshing = state.value.isRefreshing,
|
isRefreshing = state.value.isRefreshing,
|
||||||
doublePressExit = mainState.value.doublePressExit,
|
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,
|
refreshState = refreshState,
|
||||||
dialog = state.value.dialog,
|
dialog = state.value.dialog,
|
||||||
|
bottomSheet = state.value.bottomSheet,
|
||||||
|
updateCategorySort = settingsModel::onEvent,
|
||||||
|
changeLibrarySortOrder = mainModel::onEvent,
|
||||||
|
changeLibrarySortOrderDescending = mainModel::onEvent,
|
||||||
selectBook = screenModel::onEvent,
|
selectBook = screenModel::onEvent,
|
||||||
searchVisibility = screenModel::onEvent,
|
searchVisibility = screenModel::onEvent,
|
||||||
requestFocus = screenModel::onEvent,
|
requestFocus = screenModel::onEvent,
|
||||||
|
|
@ -141,6 +146,8 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
actionMoveDialog = screenModel::onEvent,
|
actionMoveDialog = screenModel::onEvent,
|
||||||
actionDeleteDialog = screenModel::onEvent,
|
actionDeleteDialog = screenModel::onEvent,
|
||||||
showDeleteDialog = screenModel::onEvent,
|
showDeleteDialog = screenModel::onEvent,
|
||||||
|
showFilterBottomSheet = screenModel::onEvent,
|
||||||
|
dismissBottomSheet = screenModel::onEvent,
|
||||||
dismissDialog = screenModel::onEvent,
|
dismissDialog = screenModel::onEvent,
|
||||||
navigateToBrowse = {
|
navigateToBrowse = {
|
||||||
navigator.push(BrowseScreen)
|
navigator.push(BrowseScreen)
|
||||||
|
|
@ -151,6 +158,9 @@ object LibraryScreen : Screen, Parcelable {
|
||||||
},
|
},
|
||||||
navigateToBookInfo = {
|
navigateToBookInfo = {
|
||||||
navigator.push(BookInfoScreen(bookId = it))
|
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 androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.library.book.SelectableBook
|
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
|
import ua.acclorite.book_story.domain.util.Dialog
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -24,5 +25,6 @@ data class LibraryState(
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val hasFocused: Boolean = false,
|
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 OnChangeProgressCount(val value: String) : MainEvent()
|
||||||
data class OnChangeHorizontalGestureAlphaAnim(val value: Boolean) : MainEvent()
|
data class OnChangeHorizontalGestureAlphaAnim(val value: Boolean) : MainEvent()
|
||||||
data class OnChangeHorizontalGesturePullAnim(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