feat: release QieKan 3.26.14 privacy mode
This commit is contained in:
+17
-1
@@ -51,7 +51,7 @@ android {
|
|||||||
applicationId = "io.legato.kazusa"
|
applicationId = "io.legato.kazusa"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = System.getenv("COMMIT_NUMBER")?.toInt()?.let { 10000 + it } ?: 32640
|
versionCode = System.getenv("COMMIT_NUMBER")?.toInt()?.let { 10000 + it } ?: 32641
|
||||||
versionName = System.getenv("APP_VERSION_NAME") ?: projectVersionName
|
versionName = System.getenv("APP_VERSION_NAME") ?: projectVersionName
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -152,6 +152,22 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants(selector().all()) { variant ->
|
||||||
|
variant.outputs.forEach { output ->
|
||||||
|
val abi = output.filters
|
||||||
|
.firstOrNull { it.filterType.name == "ABI" }
|
||||||
|
?.identifier
|
||||||
|
?: "universal"
|
||||||
|
output.outputFileName.set(
|
||||||
|
output.versionName.zip(output.versionCode) { versionName, versionCode ->
|
||||||
|
"QieKan-$abi-$versionName-vc$versionCode.apk"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain {
|
jvmToolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(21))
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">阅读·D</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="receiving_shared_label">阅读·D·搜索</string>
|
<string name="receiving_shared_label">且看·搜索</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">legado·D</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="receiving_shared_label">legado·D·search</string>
|
<string name="receiving_shared_label">且看·搜索</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -11,6 +11,7 @@ object PreferKey {
|
|||||||
const val showUnread = "showUnread"
|
const val showUnread = "showUnread"
|
||||||
const val showUnreadNew = "showUnreadNew"
|
const val showUnreadNew = "showUnreadNew"
|
||||||
const val bookGroupStyle = "bookGroupStyle"
|
const val bookGroupStyle = "bookGroupStyle"
|
||||||
|
const val bookshelfPrivacyMode = "bookshelfPrivacyMode"
|
||||||
const val useDefaultCover = "useDefaultCover"
|
const val useDefaultCover = "useDefaultCover"
|
||||||
const val loadCoverOnlyWifi = "loadCoverOnlyWifi"
|
const val loadCoverOnlyWifi = "loadCoverOnlyWifi"
|
||||||
const val coverShowName = "coverShowName"
|
const val coverShowName = "coverShowName"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 93,
|
version = 94,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
@@ -153,7 +153,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 89, to = 90),
|
AutoMigration(from = 89, to = 90),
|
||||||
AutoMigration(from = 90, to = 91),
|
AutoMigration(from = 90, to = 91),
|
||||||
AutoMigration(from = 91, to = 92),
|
AutoMigration(from = 91, to = 92),
|
||||||
AutoMigration(from = 92, to = 93)
|
AutoMigration(from = 92, to = 93),
|
||||||
|
AutoMigration(from = 93, to = 94, spec = DatabaseMigrations.Migration_93_94::class)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -496,4 +496,35 @@ object DatabaseMigrations {
|
|||||||
columnName = "enabledReview"
|
columnName = "enabledReview"
|
||||||
)
|
)
|
||||||
class Migration_64_65 : AutoMigrationSpec
|
class Migration_64_65 : AutoMigrationSpec
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
class Migration_93_94 : AutoMigrationSpec {
|
||||||
|
|
||||||
|
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||||
|
backfillBookUrl(db, "readRecord")
|
||||||
|
backfillBookUrl(db, "readRecordDetail")
|
||||||
|
backfillBookUrl(db, "readRecordSession")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backfillBookUrl(db: SupportSQLiteDatabase, tableName: String) {
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
UPDATE `$tableName`
|
||||||
|
SET bookUrl = (
|
||||||
|
SELECT MIN(book.bookUrl)
|
||||||
|
FROM books AS book
|
||||||
|
WHERE TRIM(book.name) = TRIM(`$tableName`.bookName)
|
||||||
|
AND (`$tableName`.bookAuthor = '' OR book.author = `$tableName`.bookAuthor)
|
||||||
|
)
|
||||||
|
WHERE bookUrl IS NULL
|
||||||
|
AND 1 = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM books AS book
|
||||||
|
WHERE TRIM(book.name) = TRIM(`$tableName`.bookName)
|
||||||
|
AND (`$tableName`.bookAuthor = '' OR book.author = `$tableName`.bookAuthor)
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ data class GroupBookCount(
|
|||||||
val count: Int
|
val count: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private const val USER_GROUP_ID_FILTER =
|
||||||
|
"(groupId > 0 OR groupId = ${Long.MIN_VALUE})"
|
||||||
|
|
||||||
private const val PRIVATE_GROUP_MASK =
|
private const val PRIVATE_GROUP_MASK =
|
||||||
"(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0 AND isPrivate = 1)"
|
"(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE $USER_GROUP_ID_FILTER AND isPrivate = 1)"
|
||||||
|
|
||||||
private const val PUBLIC_GROUP_MASK =
|
private const val PUBLIC_GROUP_MASK =
|
||||||
"(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0 AND isPrivate = 0)"
|
"(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE $USER_GROUP_ID_FILTER AND isPrivate = 0)"
|
||||||
|
|
||||||
private const val PUBLIC_BOOK_FILTER =
|
private const val PUBLIC_BOOK_FILTER =
|
||||||
"(`group` = 0 OR (`group` & $PRIVATE_GROUP_MASK) = 0)"
|
"(`group` = 0 OR (`group` & $PRIVATE_GROUP_MASK) = 0)"
|
||||||
@@ -162,6 +165,21 @@ interface BookDao {
|
|||||||
)
|
)
|
||||||
fun flowBookShelf(): Flow<List<BookShelfItem>>
|
fun flowBookShelf(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind, wordCount
|
||||||
|
FROM books
|
||||||
|
ORDER BY durChapterTime DESC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfIncludingPrivate(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
@Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0")
|
@Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0")
|
||||||
fun flowAudio(): Flow<List<Book>>
|
fun flowAudio(): Flow<List<Book>>
|
||||||
|
|
||||||
@@ -235,7 +253,7 @@ interface BookDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
select * from books where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
|
select * from books where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
|
||||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
and ((SELECT sum(groupId) FROM book_groups where groupId > 0 or groupId = ${Long.MIN_VALUE}) & `group`) = 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun flowNetNoGroup(): Flow<List<Book>>
|
fun flowNetNoGroup(): Flow<List<Book>>
|
||||||
@@ -276,7 +294,7 @@ interface BookDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
select * from books where type & ${BookType.local} > 0
|
select * from books where type & ${BookType.local} > 0
|
||||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
and ((SELECT sum(groupId) FROM book_groups where groupId > 0 or groupId = ${Long.MIN_VALUE}) & `group`) = 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun flowLocalNoGroup(): Flow<List<Book>>
|
fun flowLocalNoGroup(): Flow<List<Book>>
|
||||||
@@ -314,7 +332,7 @@ interface BookDao {
|
|||||||
)
|
)
|
||||||
fun flowBookShelfLocalNoGroup(): Flow<List<BookShelfItem>>
|
fun flowBookShelfLocalNoGroup(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
|
@Query("SELECT * FROM books WHERE (`group` & :group) != 0")
|
||||||
fun flowByUserGroup(group: Long): Flow<List<Book>>
|
fun flowByUserGroup(group: Long): Flow<List<Book>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
@@ -343,7 +361,7 @@ interface BookDao {
|
|||||||
kind,
|
kind,
|
||||||
wordCount
|
wordCount
|
||||||
FROM books
|
FROM books
|
||||||
WHERE (`group` & :group) > 0
|
WHERE (`group` & :group) != 0
|
||||||
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :group) = 1 OR $PUBLIC_BOOK_FILTER)
|
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :group) = 1 OR $PUBLIC_BOOK_FILTER)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -671,7 +689,7 @@ interface BookDao {
|
|||||||
)
|
)
|
||||||
fun flowBookShelfText(): Flow<List<BookShelfItem>>
|
fun flowBookShelfText(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
|
@Query("SELECT * FROM books WHERE (`group` & :group) != 0")
|
||||||
fun getBooksByGroup(group: Long): List<Book>
|
fun getBooksByGroup(group: Long): List<Book>
|
||||||
|
|
||||||
@Query("SELECT * FROM books WHERE `name` in (:names)")
|
@Query("SELECT * FROM books WHERE `name` in (:names)")
|
||||||
@@ -703,6 +721,15 @@ interface BookDao {
|
|||||||
@Query("SELECT * FROM books WHERE name = :name and author = :author")
|
@Query("SELECT * FROM books WHERE name = :name and author = :author")
|
||||||
fun getBook(name: String, author: String): Book?
|
fun getBook(name: String, author: String): Book?
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM books
|
||||||
|
WHERE name = :name AND author = :author
|
||||||
|
ORDER BY durChapterTime DESC, bookUrl ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getBooks(name: String, author: String): List<Book>
|
||||||
|
|
||||||
@Query("""select distinct bs.* from books, book_sources bs
|
@Query("""select distinct bs.* from books, book_sources bs
|
||||||
where origin == bookSourceUrl and origin not like '${BookType.localTag}%'
|
where origin == bookSourceUrl and origin not like '${BookType.localTag}%'
|
||||||
and origin not like '${BookType.webDavTag}%'""")
|
and origin not like '${BookType.webDavTag}%'""")
|
||||||
@@ -774,7 +801,7 @@ interface BookDao {
|
|||||||
@Query("update books set `group` = :newGroupId where `group` = :oldGroupId")
|
@Query("update books set `group` = :newGroupId where `group` = :oldGroupId")
|
||||||
fun upGroup(oldGroupId: Long, newGroupId: Long)
|
fun upGroup(oldGroupId: Long, newGroupId: Long)
|
||||||
|
|
||||||
@Query("update books set `group` = `group` - :group where `group` & :group > 0")
|
@Query("update books set `group` = `group` & ~:group where `group` & :group != 0")
|
||||||
fun removeGroup(group: Long)
|
fun removeGroup(group: Long)
|
||||||
|
|
||||||
@Query("delete from books where type & ${BookType.notShelf} > 0")
|
@Query("delete from books where type & ${BookType.notShelf} > 0")
|
||||||
@@ -818,7 +845,7 @@ interface BookDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT COUNT(*) FROM books
|
SELECT COUNT(*) FROM books
|
||||||
WHERE (`group` & :groupId) > 0
|
WHERE (`group` & :groupId) != 0
|
||||||
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
|
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -1096,7 +1123,7 @@ interface BookDao {
|
|||||||
type, `group`, `order`, canUpdate,
|
type, `group`, `order`, canUpdate,
|
||||||
ifnull(customIntro, intro) as intro, kind, wordCount
|
ifnull(customIntro, intro) as intro, kind, wordCount
|
||||||
FROM books
|
FROM books
|
||||||
WHERE (`group` & :groupId) > 0
|
WHERE (`group` & :groupId) != 0
|
||||||
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
|
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
|
||||||
ORDER BY durChapterTime DESC
|
ORDER BY durChapterTime DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
|
|||||||
@@ -27,16 +27,16 @@ interface BookGroupDao {
|
|||||||
"""
|
"""
|
||||||
with const as (
|
with const as (
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(CASE WHEN groupId > 0 AND isPrivate = 0 THEN groupId ELSE 0 END), 0) sumPublicGroupId,
|
COALESCE(SUM(CASE WHEN (groupId > 0 OR groupId = ${Long.MIN_VALUE}) AND isPrivate = 0 THEN groupId ELSE 0 END), 0) sumPublicGroupId,
|
||||||
COALESCE(SUM(CASE WHEN groupId > 0 AND isPrivate = 1 THEN groupId ELSE 0 END), 0) sumPrivateGroupId
|
COALESCE(SUM(CASE WHEN (groupId > 0 OR groupId = ${Long.MIN_VALUE}) AND isPrivate = 1 THEN groupId ELSE 0 END), 0) sumPrivateGroupId
|
||||||
FROM book_groups
|
FROM book_groups
|
||||||
)
|
)
|
||||||
SELECT book_groups.* FROM book_groups join const
|
SELECT book_groups.* FROM book_groups join const
|
||||||
where show > 0
|
where show > 0
|
||||||
and (
|
and (
|
||||||
(groupId >= 0 and exists (
|
((groupId >= 0 or groupId = ${Long.MIN_VALUE}) and exists (
|
||||||
select 1 from books
|
select 1 from books
|
||||||
where `group` & book_groups.groupId > 0
|
where `group` & book_groups.groupId != 0
|
||||||
and (book_groups.isPrivate = 1 or `group` = 0 or (const.sumPrivateGroupId & `group`) = 0)
|
and (book_groups.isPrivate = 1 or `group` = 0 or (const.sumPrivateGroupId & `group`) = 0)
|
||||||
))
|
))
|
||||||
or groupId = ${BookGroup.IdAll}
|
or groupId = ${BookGroup.IdAll}
|
||||||
@@ -124,13 +124,13 @@ interface BookGroupDao {
|
|||||||
)
|
)
|
||||||
val show: LiveData<List<BookGroup>>
|
val show: LiveData<List<BookGroup>>
|
||||||
|
|
||||||
@Query("SELECT * FROM book_groups where groupId >= 0 ORDER BY `order`")
|
@Query("SELECT * FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE} ORDER BY `order`")
|
||||||
fun flowSelect(): Flow<List<BookGroup>>
|
fun flowSelect(): Flow<List<BookGroup>>
|
||||||
|
|
||||||
@get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0")
|
@get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE}")
|
||||||
val idsSum: Long
|
val idsSum: Long
|
||||||
|
|
||||||
@get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0")
|
@get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE}")
|
||||||
val maxOrder: Int
|
val maxOrder: Int
|
||||||
|
|
||||||
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
|
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
|
||||||
@@ -142,7 +142,7 @@ interface BookGroupDao {
|
|||||||
@Query("update book_groups set show = 1 where groupId = :groupId")
|
@Query("update book_groups set show = 1 where groupId = :groupId")
|
||||||
fun enableGroup(groupId: Long)
|
fun enableGroup(groupId: Long)
|
||||||
|
|
||||||
@Query("select groupName from book_groups where groupId > 0 and (groupId & :id) > 0")
|
@Query("select groupName from book_groups where (groupId > 0 or groupId = ${Long.MIN_VALUE}) and (groupId & :id) != 0")
|
||||||
fun getGroupNames(id: Long): List<String>
|
fun getGroupNames(id: Long): List<String>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Delete
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.legado.app.data.entities.readRecord.HomeRecentBookRow
|
import io.legado.app.data.entities.readRecord.HomeRecentBookRow
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
@@ -44,17 +45,26 @@ interface ReadRecordDao {
|
|||||||
"""
|
"""
|
||||||
WITH recent AS (
|
WITH recent AS (
|
||||||
SELECT
|
SELECT
|
||||||
bookName,
|
base.bookName,
|
||||||
bookAuthor,
|
base.bookAuthor,
|
||||||
MAX(lastRead) AS lastRead
|
(
|
||||||
FROM readRecord
|
SELECT latest.bookUrl
|
||||||
GROUP BY bookName, bookAuthor
|
FROM readRecord AS latest
|
||||||
ORDER BY lastRead DESC
|
WHERE latest.bookName = base.bookName
|
||||||
LIMIT :limit
|
AND latest.bookAuthor = base.bookAuthor
|
||||||
|
AND latest.bookUrl IS NOT NULL
|
||||||
|
AND latest.bookUrl != ''
|
||||||
|
ORDER BY latest.lastRead DESC, latest.deviceId ASC
|
||||||
|
LIMIT 1
|
||||||
|
) AS recordBookUrl,
|
||||||
|
MAX(base.lastRead) AS lastRead
|
||||||
|
FROM readRecord AS base
|
||||||
|
GROUP BY base.bookName, base.bookAuthor
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
recent.bookName AS recordName,
|
recent.bookName AS recordName,
|
||||||
recent.bookAuthor AS recordAuthor,
|
recent.bookAuthor AS recordAuthor,
|
||||||
|
recent.recordBookUrl AS recordBookUrl,
|
||||||
book.bookUrl AS bookUrl,
|
book.bookUrl AS bookUrl,
|
||||||
book.origin AS origin,
|
book.origin AS origin,
|
||||||
book.coverUrl AS coverUrl,
|
book.coverUrl AS coverUrl,
|
||||||
@@ -63,7 +73,14 @@ interface ReadRecordDao {
|
|||||||
book.totalChapterNum AS totalChapterNum,
|
book.totalChapterNum AS totalChapterNum,
|
||||||
book.durChapterIndex AS chapterIndex
|
book.durChapterIndex AS chapterIndex
|
||||||
FROM recent
|
FROM recent
|
||||||
LEFT JOIN books AS book ON book.bookUrl = (
|
LEFT JOIN books AS book ON book.bookUrl = COALESCE(
|
||||||
|
(
|
||||||
|
SELECT currentBook.bookUrl
|
||||||
|
FROM books AS currentBook
|
||||||
|
WHERE currentBook.bookUrl = recent.recordBookUrl
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(
|
||||||
SELECT candidate.bookUrl
|
SELECT candidate.bookUrl
|
||||||
FROM books AS candidate
|
FROM books AS candidate
|
||||||
WHERE candidate.name = recent.bookName
|
WHERE candidate.name = recent.bookName
|
||||||
@@ -71,10 +88,11 @@ interface ReadRecordDao {
|
|||||||
ORDER BY candidate.durChapterTime DESC, candidate.bookUrl ASC
|
ORDER BY candidate.durChapterTime DESC, candidate.bookUrl ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
|
)
|
||||||
ORDER BY recent.lastRead DESC
|
ORDER BY recent.lastRead DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun observeRecentHomeBooks(limit: Int): Flow<List<HomeRecentBookRow>>
|
fun observeRecentHomeBooks(): Flow<List<HomeRecentBookRow>>
|
||||||
|
|
||||||
@Query("select sum(readTime) from readRecord where bookName = :bookName")
|
@Query("select sum(readTime) from readRecord where bookName = :bookName")
|
||||||
fun getReadTime(bookName: String): Long?
|
fun getReadTime(bookName: String): Long?
|
||||||
@@ -85,6 +103,33 @@ interface ReadRecordDao {
|
|||||||
@Query("SELECT * FROM readRecord WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor")
|
@Query("SELECT * FROM readRecord WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor")
|
||||||
suspend fun getReadRecord(deviceId: String, bookName: String, bookAuthor: String): ReadRecord?
|
suspend fun getReadRecord(deviceId: String, bookName: String, bookAuthor: String): ReadRecord?
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl FROM books
|
||||||
|
WHERE TRIM(name) = TRIM(:bookName)
|
||||||
|
AND (:bookAuthor = '' OR author = :bookAuthor)
|
||||||
|
ORDER BY durChapterTime DESC, bookUrl ASC
|
||||||
|
LIMIT 2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
suspend fun findMatchingBookUrls(bookName: String, bookAuthor: String): List<String>
|
||||||
|
|
||||||
|
@Query("UPDATE readRecord SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
|
||||||
|
fun replaceReadRecordBookUrl(oldBookUrl: String, newBookUrl: String)
|
||||||
|
|
||||||
|
@Query("UPDATE readRecordDetail SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
|
||||||
|
fun replaceReadRecordDetailBookUrl(oldBookUrl: String, newBookUrl: String)
|
||||||
|
|
||||||
|
@Query("UPDATE readRecordSession SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
|
||||||
|
fun replaceReadRecordSessionBookUrl(oldBookUrl: String, newBookUrl: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun replaceBookUrl(oldBookUrl: String, newBookUrl: String) {
|
||||||
|
replaceReadRecordBookUrl(oldBookUrl, newBookUrl)
|
||||||
|
replaceReadRecordDetailBookUrl(oldBookUrl, newBookUrl)
|
||||||
|
replaceReadRecordSessionBookUrl(oldBookUrl, newBookUrl)
|
||||||
|
}
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(vararg readRecord: ReadRecord)
|
suspend fun insert(vararg readRecord: ReadRecord)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.legado.app.data.entities.readRecord
|
|||||||
data class HomeRecentBookRow(
|
data class HomeRecentBookRow(
|
||||||
val recordName: String,
|
val recordName: String,
|
||||||
val recordAuthor: String,
|
val recordAuthor: String,
|
||||||
|
val recordBookUrl: String?,
|
||||||
val bookUrl: String?,
|
val bookUrl: String?,
|
||||||
val origin: String?,
|
val origin: String?,
|
||||||
val coverUrl: String?,
|
val coverUrl: String?,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class ReadRecord(
|
|||||||
var bookName: String = "",
|
var bookName: String = "",
|
||||||
@ColumnInfo(defaultValue = "")
|
@ColumnInfo(defaultValue = "")
|
||||||
var bookAuthor: String = "",
|
var bookAuthor: String = "",
|
||||||
|
var bookUrl: String? = null,
|
||||||
@ColumnInfo(defaultValue = "0")
|
@ColumnInfo(defaultValue = "0")
|
||||||
var readTime: Long = 0L,
|
var readTime: Long = 0L,
|
||||||
@ColumnInfo(defaultValue = "0")
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ data class ReadRecordDetail(
|
|||||||
val bookName: String = "",
|
val bookName: String = "",
|
||||||
@ColumnInfo(defaultValue = "")
|
@ColumnInfo(defaultValue = "")
|
||||||
val bookAuthor: String = "",
|
val bookAuthor: String = "",
|
||||||
|
val bookUrl: String? = null,
|
||||||
val date: String = "",
|
val date: String = "",
|
||||||
|
|
||||||
// 当天阅读总时长
|
// 当天阅读总时长
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ data class ReadRecordSession(
|
|||||||
val bookName: String = "",
|
val bookName: String = "",
|
||||||
@ColumnInfo(defaultValue = "")
|
@ColumnInfo(defaultValue = "")
|
||||||
val bookAuthor: String = "",
|
val bookAuthor: String = "",
|
||||||
|
val bookUrl: String? = null,
|
||||||
|
|
||||||
// 一次阅读的开始/结束
|
// 一次阅读的开始/结束
|
||||||
val startTime: Long = 0,
|
val startTime: Long = 0,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import io.legado.app.help.book.ContentProcessor
|
|||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.MD5Utils
|
import io.legado.app.utils.MD5Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AiToolRepository(
|
class AiToolRepository(
|
||||||
@@ -27,7 +28,8 @@ class AiToolRepository(
|
|||||||
private val bookmarkDao: BookmarkDao,
|
private val bookmarkDao: BookmarkDao,
|
||||||
private val readRecordDao: ReadRecordDao,
|
private val readRecordDao: ReadRecordDao,
|
||||||
private val aiArtifactDao: AiArtifactDao,
|
private val aiArtifactDao: AiArtifactDao,
|
||||||
private val aiMemoryGateway: AiMemoryGateway
|
private val aiMemoryGateway: AiMemoryGateway,
|
||||||
|
private val bookPrivacyRepository: BookPrivacyRepository,
|
||||||
) : AiToolGateway {
|
) : AiToolGateway {
|
||||||
|
|
||||||
override fun availableTools(): List<AiToolDefinition> = tools
|
override fun availableTools(): List<AiToolDefinition> = tools
|
||||||
@@ -175,11 +177,15 @@ class AiToolRepository(
|
|||||||
return GSON.toJson(mapOf("bookmarks" to bookmarks))
|
return GSON.toJson(mapOf("bookmarks" to bookmarks))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getReadingStats(args: JsonObject): String {
|
private suspend fun getReadingStats(args: JsonObject): String {
|
||||||
val query = args.string("query").orEmpty().trim()
|
val query = args.string("query").orEmpty().trim()
|
||||||
val date = args.string("date")?.trim().orEmpty()
|
val date = args.string("date")?.trim().orEmpty()
|
||||||
val limit = args.int("limit", 10).coerceIn(1, 30)
|
val limit = args.int("limit", 10).coerceIn(1, 30)
|
||||||
val records = readRecordDao.all
|
val privacyState = bookPrivacyRepository.observe().first()
|
||||||
|
val visibleRecords = readRecordDao.all.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
|
val records = visibleRecords
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter {
|
||||||
query.isBlank() ||
|
query.isBlank() ||
|
||||||
@@ -200,6 +206,7 @@ class AiToolRepository(
|
|||||||
val dailyDetails = readRecordDao.allDetail
|
val dailyDetails = readRecordDao.allDetail
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor) &&
|
||||||
(date.isBlank() || it.date == date) &&
|
(date.isBlank() || it.date == date) &&
|
||||||
(query.isBlank() ||
|
(query.isBlank() ||
|
||||||
it.bookName.contains(query, ignoreCase = true) ||
|
it.bookName.contains(query, ignoreCase = true) ||
|
||||||
@@ -222,7 +229,7 @@ class AiToolRepository(
|
|||||||
.toList()
|
.toList()
|
||||||
return GSON.toJson(
|
return GSON.toJson(
|
||||||
mapOf(
|
mapOf(
|
||||||
"totalReadTimeMillis" to readRecordDao.all.sumOf { it.readTime },
|
"totalReadTimeMillis" to visibleRecords.sumOf { it.readTime },
|
||||||
"recentRecords" to records,
|
"recentRecords" to records,
|
||||||
"dailyDetails" to dailyDetails
|
"dailyDetails" to dailyDetails
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.data.dao.BookDao
|
||||||
|
import io.legado.app.data.dao.BookGroupDao
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import java.text.Normalizer
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class BookRecordKey(
|
||||||
|
val name: String,
|
||||||
|
val author: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookPrivacyState(
|
||||||
|
val privacyModeEnabled: Boolean,
|
||||||
|
val privateGroupMask: Long,
|
||||||
|
val visibleBookUrls: Set<String>,
|
||||||
|
val visibleBookKeys: Set<BookRecordKey>,
|
||||||
|
val visibleBookNames: Set<String>,
|
||||||
|
val hiddenBookUrls: Set<String>,
|
||||||
|
val hiddenBookKeys: Set<BookRecordKey>,
|
||||||
|
val hiddenBookNames: Set<String>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isReadRecordVisible(
|
||||||
|
bookUrl: String?,
|
||||||
|
bookName: String,
|
||||||
|
bookAuthor: String = "",
|
||||||
|
): Boolean {
|
||||||
|
if (!privacyModeEnabled) return true
|
||||||
|
if (!bookUrl.isNullOrBlank()) {
|
||||||
|
if (bookUrl in hiddenBookUrls) return false
|
||||||
|
if (bookUrl in visibleBookUrls) return true
|
||||||
|
}
|
||||||
|
val normalizedName = normalizeBookName(bookName)
|
||||||
|
val normalizedAuthor = normalizeBookName(bookAuthor)
|
||||||
|
if (normalizedAuthor.isNotEmpty()) {
|
||||||
|
val key = BookRecordKey(normalizedName, normalizedAuthor)
|
||||||
|
if (key in hiddenBookKeys) return false
|
||||||
|
if (key in visibleBookKeys) return true
|
||||||
|
}
|
||||||
|
return normalizedName in visibleBookNames && normalizedName !in hiddenBookNames
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(
|
||||||
|
books: List<Book>,
|
||||||
|
groups: List<BookGroup>,
|
||||||
|
privacyModeEnabled: Boolean,
|
||||||
|
): BookPrivacyState {
|
||||||
|
val privateGroupMask = groups.asSequence()
|
||||||
|
.filter {
|
||||||
|
(it.groupId > 0L || it.groupId == Long.MIN_VALUE) && it.isPrivate
|
||||||
|
}
|
||||||
|
.fold(0L) { mask, group -> mask or group.groupId }
|
||||||
|
val hiddenBooks = books.filter { (it.group and privateGroupMask) != 0L }
|
||||||
|
val visibleBooks = books.filterNot { (it.group and privateGroupMask) != 0L }
|
||||||
|
return BookPrivacyState(
|
||||||
|
privacyModeEnabled = privacyModeEnabled,
|
||||||
|
privateGroupMask = privateGroupMask,
|
||||||
|
visibleBookUrls = visibleBooks.mapTo(hashSetOf()) { it.bookUrl },
|
||||||
|
visibleBookKeys = visibleBooks.mapTo(hashSetOf()) {
|
||||||
|
BookRecordKey(normalizeBookName(it.name), normalizeBookName(it.author))
|
||||||
|
},
|
||||||
|
visibleBookNames = visibleBooks.mapTo(hashSetOf()) {
|
||||||
|
normalizeBookName(it.name)
|
||||||
|
},
|
||||||
|
hiddenBookUrls = hiddenBooks.mapTo(hashSetOf()) { it.bookUrl },
|
||||||
|
hiddenBookKeys = hiddenBooks.mapTo(hashSetOf()) {
|
||||||
|
BookRecordKey(normalizeBookName(it.name), normalizeBookName(it.author))
|
||||||
|
},
|
||||||
|
hiddenBookNames = hiddenBooks.mapTo(hashSetOf()) {
|
||||||
|
normalizeBookName(it.name)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookPrivacyRepository(
|
||||||
|
private val bookDao: BookDao,
|
||||||
|
private val bookGroupDao: BookGroupDao,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun observe(): Flow<BookPrivacyState> = combine(
|
||||||
|
bookDao.flowAll(),
|
||||||
|
bookGroupDao.flowAll(),
|
||||||
|
settingsRepository.getBoolean(PreferKey.bookshelfPrivacyMode, true),
|
||||||
|
BookPrivacyState::create,
|
||||||
|
).distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun normalizeBookName(name: String): String =
|
||||||
|
Normalizer.normalize(name, Normalizer.Form.NFKC)
|
||||||
|
.lowercase(Locale.ROOT)
|
||||||
|
.filterNot { it.isWhitespace() || it == '\u200B' || it == '\uFEFF' }
|
||||||
@@ -8,32 +8,48 @@ import io.legado.app.data.entities.BookChapter
|
|||||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class BookRepository(
|
class BookRepository(
|
||||||
private val bookDao: BookDao,
|
private val bookDao: BookDao,
|
||||||
private val bookChapterDao: BookChapterDao
|
private val bookChapterDao: BookChapterDao,
|
||||||
|
private val bookPrivacyRepository: BookPrivacyRepository,
|
||||||
) {
|
) {
|
||||||
fun getAllBooks(): Flow<List<Book>> {
|
fun getAllBooks(): Flow<List<Book>> {
|
||||||
return bookDao.flowAll()
|
return bookDao.flowAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
|
fun getVisibleBooks(): Flow<List<Book>> {
|
||||||
return withContext(Dispatchers.IO) {
|
return combine(bookDao.flowAll(), bookPrivacyRepository.observe()) { books, privacyState ->
|
||||||
bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
|
books.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.name, it.author)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
|
||||||
|
return getVisibleBook(bookName, bookAuthor)?.getDisplayCover()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getChapterTitle(bookName: String, bookAuthor: String, chapterIndex: Int): String? {
|
suspend fun getChapterTitle(bookName: String, bookAuthor: String, chapterIndex: Int): String? {
|
||||||
|
val bookUrl = getVisibleBook(bookName, bookAuthor)?.bookUrl ?: return null
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val book = bookDao.getBook(bookName, bookAuthor)
|
|
||||||
val bookUrl = book?.bookUrl
|
|
||||||
if (bookUrl.isNullOrEmpty()) return@withContext null
|
|
||||||
|
|
||||||
bookChapterDao.getChapterTitleByUrlAndIndex(bookUrl, chapterIndex)
|
bookChapterDao.getChapterTitleByUrlAndIndex(bookUrl, chapterIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getVisibleBook(name: String, author: String): Book? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val privacyState = bookPrivacyRepository.observe().first()
|
||||||
|
bookDao.getBooks(name, author).firstOrNull {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.name, it.author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getBook(bookUrl: String): Book? {
|
suspend fun getBook(bookUrl: String): Book? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
bookDao.getBook(bookUrl)
|
bookDao.getBook(bookUrl)
|
||||||
@@ -50,6 +66,12 @@ class BookRepository(
|
|||||||
return bookDao.flowBookShelfByGroup(groupId)
|
return bookDao.flowBookShelfByGroup(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun flowBookShelfIncludingPrivate(): Flow<List<BookShelfItem>> {
|
||||||
|
return bookDao.flowBookShelfIncludingPrivate().map { books ->
|
||||||
|
books.filterNot { it.isNotShelf }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun flowSystemGroupCounts(): Flow<List<GroupBookCount>> {
|
fun flowSystemGroupCounts(): Flow<List<GroupBookCount>> {
|
||||||
return bookDao.flowSystemGroupCounts()
|
return bookDao.flowSystemGroupCounts()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,25 +8,40 @@ import io.legado.app.domain.model.DEFAULT_HOME_DASHBOARD_SECTIONS
|
|||||||
import io.legado.app.domain.model.HomeDashboardSection
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
import io.legado.app.domain.model.HomeReadingBook
|
import io.legado.app.domain.model.HomeReadingBook
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class HomeDashboardRepository(
|
class HomeDashboardRepository(
|
||||||
private val readRecordDao: ReadRecordDao,
|
private val readRecordDao: ReadRecordDao,
|
||||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||||
|
private val readRecordRepository: ReadRecordRepository,
|
||||||
|
private val bookPrivacyRepository: BookPrivacyRepository,
|
||||||
) : HomeDashboardGateway {
|
) : HomeDashboardGateway {
|
||||||
|
|
||||||
override fun observeTotalReadBooks(): Flow<Int> =
|
override fun observeTotalReadBooks(): Flow<Int> =
|
||||||
readRecordDao.observeTotalReadBookCount()
|
readRecordRepository.getTotalReadBookCount()
|
||||||
|
|
||||||
override fun observeTotalReadTime(): Flow<Long> =
|
override fun observeTotalReadTime(): Flow<Long> =
|
||||||
readRecordDao.getTotalReadTime().map { it ?: 0L }
|
readRecordRepository.getTotalReadTime()
|
||||||
|
|
||||||
override fun observeReadTime(date: String): Flow<Long> =
|
override fun observeReadTime(date: String): Flow<Long> =
|
||||||
readRecordDao.observeReadTimeByDate(date).map { it ?: 0L }
|
readRecordRepository.getReadTime(date)
|
||||||
|
|
||||||
override fun observeRecentBooks(limit: Int): Flow<List<HomeReadingBook>> =
|
override fun observeRecentBooks(limit: Int): Flow<List<HomeReadingBook>> {
|
||||||
readRecordDao.observeRecentHomeBooks(limit).map { rows ->
|
return combine(
|
||||||
rows.map { row ->
|
readRecordDao.observeRecentHomeBooks(),
|
||||||
|
bookPrivacyRepository.observe(),
|
||||||
|
) { rows, privacyState ->
|
||||||
|
rows.asSequence()
|
||||||
|
.filter { row ->
|
||||||
|
privacyState.isReadRecordVisible(
|
||||||
|
row.recordBookUrl,
|
||||||
|
row.recordName,
|
||||||
|
row.recordAuthor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.take(limit)
|
||||||
|
.map { row ->
|
||||||
HomeReadingBook(
|
HomeReadingBook(
|
||||||
bookUrl = row.bookUrl,
|
bookUrl = row.bookUrl,
|
||||||
name = row.recordName,
|
name = row.recordName,
|
||||||
@@ -51,6 +66,8 @@ class HomeDashboardRepository(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeDailyGoal(defaultValue: Int): Flow<Int> =
|
override fun observeDailyGoal(defaultValue: Int): Flow<Int> =
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
|||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -18,6 +20,7 @@ import kotlin.math.min
|
|||||||
class ReadRecordRepository(
|
class ReadRecordRepository(
|
||||||
private val dao: ReadRecordDao,
|
private val dao: ReadRecordDao,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
|
private val bookPrivacyRepository: BookPrivacyRepository,
|
||||||
) {
|
) {
|
||||||
private fun getCurrentDeviceId(): String = ""
|
private fun getCurrentDeviceId(): String = ""
|
||||||
|
|
||||||
@@ -25,37 +28,73 @@ class ReadRecordRepository(
|
|||||||
* 获取总阅读时长流
|
* 获取总阅读时长流
|
||||||
*/
|
*/
|
||||||
fun getTotalReadTime(): Flow<Long> {
|
fun getTotalReadTime(): Flow<Long> {
|
||||||
return dao.getTotalReadTime().map { it ?: 0L }
|
return getLatestReadRecords().map { records -> records.sumOf { it.readTime } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTotalReadBookCount(): Flow<Int> {
|
||||||
|
return getLatestReadRecords().map { records ->
|
||||||
|
records.distinctBy { it.bookName to it.bookAuthor }.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReadTime(date: String): Flow<Long> {
|
||||||
|
return getAllRecordDetails().map { details ->
|
||||||
|
details.filter { it.date == date }.sumOf { it.readTime }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据搜索关键字获取最新的阅读书籍列表流
|
* 根据搜索关键字获取最新的阅读书籍列表流
|
||||||
*/
|
*/
|
||||||
fun getLatestReadRecords(query: String = ""): Flow<List<ReadRecord>> {
|
fun getLatestReadRecords(query: String = ""): Flow<List<ReadRecord>> {
|
||||||
return if (query.isBlank()) {
|
val records = if (query.isBlank()) {
|
||||||
dao.getAllReadRecordsSortedByLastRead()
|
dao.getAllReadRecordsSortedByLastRead()
|
||||||
} else {
|
} else {
|
||||||
dao.searchReadRecordsByLastRead(query)
|
dao.searchReadRecordsByLastRead(query)
|
||||||
}
|
}
|
||||||
|
return combine(records, bookPrivacyRepository.observe()) { items, privacyState ->
|
||||||
|
items.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有的每日统计详情流
|
* 获取所有的每日统计详情流
|
||||||
*/
|
*/
|
||||||
fun getAllRecordDetails(query: String = ""): Flow<List<ReadRecordDetail>> {
|
fun getAllRecordDetails(query: String = ""): Flow<List<ReadRecordDetail>> {
|
||||||
return if (query.isBlank()) {
|
val details = if (query.isBlank()) {
|
||||||
dao.getAllDetails()
|
dao.getAllDetails()
|
||||||
} else {
|
} else {
|
||||||
dao.searchDetails(query)
|
dao.searchDetails(query)
|
||||||
}
|
}
|
||||||
|
return combine(details, bookPrivacyRepository.observe()) { items, privacyState ->
|
||||||
|
items.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllSessions(): Flow<List<ReadRecordSession>> {
|
fun getAllSessions(): Flow<List<ReadRecordSession>> {
|
||||||
return dao.getAllSessions(getCurrentDeviceId())
|
return combine(
|
||||||
|
dao.getAllSessions(getCurrentDeviceId()),
|
||||||
|
bookPrivacyRepository.observe(),
|
||||||
|
) { sessions, privacyState ->
|
||||||
|
sessions.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBookSessions(bookName: String, bookAuthor: String): Flow<List<ReadRecordSession>> {
|
fun getBookSessions(bookName: String, bookAuthor: String): Flow<List<ReadRecordSession>> {
|
||||||
return dao.getSessionsByBookFlow(getCurrentDeviceId(), bookName, bookAuthor)
|
return combine(
|
||||||
|
dao.getSessionsByBookFlow(getCurrentDeviceId(), bookName, bookAuthor),
|
||||||
|
bookPrivacyRepository.observe(),
|
||||||
|
) { sessions, privacyState ->
|
||||||
|
sessions.filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBookTimelineDays(bookName: String, bookAuthor: String): Flow<List<ReadRecordTimelineDay>> {
|
fun getBookTimelineDays(bookName: String, bookAuthor: String): Flow<List<ReadRecordTimelineDay>> {
|
||||||
@@ -72,15 +111,27 @@ class ReadRecordRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getBookReadTime(bookName: String, bookAuthor: String): Flow<Long> {
|
fun getBookReadTime(bookName: String, bookAuthor: String): Flow<Long> {
|
||||||
return dao.getReadTimeFlow(getCurrentDeviceId(), bookName, bookAuthor).map { it ?: 0L }
|
return combine(
|
||||||
|
dao.getReadTimeFlow(getCurrentDeviceId(), bookName, bookAuthor),
|
||||||
|
bookPrivacyRepository.observe(),
|
||||||
|
) { readTime, privacyState ->
|
||||||
|
if (privacyState.isReadRecordVisible(null, bookName, bookAuthor)) {
|
||||||
|
readTime ?: 0L
|
||||||
|
} else {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMergeCandidates(targetRecord: ReadRecord): List<ReadRecord> {
|
suspend fun getMergeCandidates(targetRecord: ReadRecord): List<ReadRecord> {
|
||||||
|
val privacyState = bookPrivacyRepository.observe().first()
|
||||||
return dao.getReadRecordsByNameExcludingAuthor(
|
return dao.getReadRecordsByNameExcludingAuthor(
|
||||||
targetRecord.deviceId,
|
targetRecord.deviceId,
|
||||||
targetRecord.bookName,
|
targetRecord.bookName,
|
||||||
targetRecord.bookAuthor
|
targetRecord.bookAuthor
|
||||||
)
|
).filter {
|
||||||
|
privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +155,7 @@ class ReadRecordRepository(
|
|||||||
if (existingRecord != null) {
|
if (existingRecord != null) {
|
||||||
dao.update(
|
dao.update(
|
||||||
existingRecord.copy(
|
existingRecord.copy(
|
||||||
|
bookUrl = session.bookUrl ?: existingRecord.bookUrl,
|
||||||
readTime = existingRecord.readTime + durationDelta,
|
readTime = existingRecord.readTime + durationDelta,
|
||||||
lastRead = session.endTime
|
lastRead = session.endTime
|
||||||
)
|
)
|
||||||
@@ -114,6 +166,7 @@ class ReadRecordRepository(
|
|||||||
deviceId = session.deviceId,
|
deviceId = session.deviceId,
|
||||||
bookName = session.bookName,
|
bookName = session.bookName,
|
||||||
bookAuthor = session.bookAuthor,
|
bookAuthor = session.bookAuthor,
|
||||||
|
bookUrl = session.bookUrl,
|
||||||
readTime = durationDelta,
|
readTime = durationDelta,
|
||||||
lastRead = session.endTime
|
lastRead = session.endTime
|
||||||
)
|
)
|
||||||
@@ -135,17 +188,22 @@ class ReadRecordRepository(
|
|||||||
dateString
|
dateString
|
||||||
)
|
)
|
||||||
if (existingDetail != null) {
|
if (existingDetail != null) {
|
||||||
existingDetail.readTime += durationDelta
|
dao.insertDetail(
|
||||||
existingDetail.readWords += wordsDelta
|
existingDetail.copy(
|
||||||
existingDetail.firstReadTime = min(existingDetail.firstReadTime, session.startTime)
|
bookUrl = session.bookUrl ?: existingDetail.bookUrl,
|
||||||
existingDetail.lastReadTime = max(existingDetail.lastReadTime, session.endTime)
|
readTime = existingDetail.readTime + durationDelta,
|
||||||
dao.insertDetail(existingDetail)
|
readWords = existingDetail.readWords + wordsDelta,
|
||||||
|
firstReadTime = min(existingDetail.firstReadTime, session.startTime),
|
||||||
|
lastReadTime = max(existingDetail.lastReadTime, session.endTime),
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
dao.insertDetail(
|
dao.insertDetail(
|
||||||
ReadRecordDetail(
|
ReadRecordDetail(
|
||||||
deviceId = session.deviceId,
|
deviceId = session.deviceId,
|
||||||
bookName = session.bookName,
|
bookName = session.bookName,
|
||||||
bookAuthor = session.bookAuthor,
|
bookAuthor = session.bookAuthor,
|
||||||
|
bookUrl = session.bookUrl,
|
||||||
date = dateString,
|
date = dateString,
|
||||||
readTime = durationDelta,
|
readTime = durationDelta,
|
||||||
readWords = wordsDelta,
|
readWords = wordsDelta,
|
||||||
@@ -204,6 +262,7 @@ class ReadRecordRepository(
|
|||||||
)
|
)
|
||||||
dao.insertDetail(
|
dao.insertDetail(
|
||||||
existingDetail?.copy(
|
existingDetail?.copy(
|
||||||
|
bookUrl = existingDetail.bookUrl ?: remainingSessions.resolvedBookUrl(),
|
||||||
readTime = totalTime,
|
readTime = totalTime,
|
||||||
readWords = totalWords,
|
readWords = totalWords,
|
||||||
firstReadTime = firstRead,
|
firstReadTime = firstRead,
|
||||||
@@ -212,6 +271,7 @@ class ReadRecordRepository(
|
|||||||
deviceId = session.deviceId,
|
deviceId = session.deviceId,
|
||||||
bookName = session.bookName,
|
bookName = session.bookName,
|
||||||
bookAuthor = session.bookAuthor,
|
bookAuthor = session.bookAuthor,
|
||||||
|
bookUrl = remainingSessions.resolvedBookUrl(),
|
||||||
date = dateString,
|
date = dateString,
|
||||||
readTime = totalTime,
|
readTime = totalTime,
|
||||||
readWords = totalWords,
|
readWords = totalWords,
|
||||||
@@ -233,6 +293,7 @@ class ReadRecordRepository(
|
|||||||
} else {
|
} else {
|
||||||
val totalTime = allRemainingSessions.sumOf { it.endTime - it.startTime }
|
val totalTime = allRemainingSessions.sumOf { it.endTime - it.startTime }
|
||||||
val lastRead = allRemainingSessions.maxOf { it.endTime }
|
val lastRead = allRemainingSessions.maxOf { it.endTime }
|
||||||
|
val bookUrl = allRemainingSessions.resolvedBookUrl()
|
||||||
|
|
||||||
val existingRecord = dao.getReadRecord(deviceId, bookName, bookAuthor)
|
val existingRecord = dao.getReadRecord(deviceId, bookName, bookAuthor)
|
||||||
if (existingRecord == null) {
|
if (existingRecord == null) {
|
||||||
@@ -241,6 +302,7 @@ class ReadRecordRepository(
|
|||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
bookName = bookName,
|
bookName = bookName,
|
||||||
bookAuthor = bookAuthor,
|
bookAuthor = bookAuthor,
|
||||||
|
bookUrl = bookUrl,
|
||||||
readTime = totalTime,
|
readTime = totalTime,
|
||||||
lastRead = lastRead,
|
lastRead = lastRead,
|
||||||
)
|
)
|
||||||
@@ -248,6 +310,7 @@ class ReadRecordRepository(
|
|||||||
} else {
|
} else {
|
||||||
dao.update(
|
dao.update(
|
||||||
existingRecord.copy(
|
existingRecord.copy(
|
||||||
|
bookUrl = existingRecord.bookUrl ?: bookUrl,
|
||||||
readTime = totalTime,
|
readTime = totalTime,
|
||||||
lastRead = lastRead
|
lastRead = lastRead
|
||||||
)
|
)
|
||||||
@@ -288,9 +351,11 @@ class ReadRecordRepository(
|
|||||||
targetRecord.bookName,
|
targetRecord.bookName,
|
||||||
targetRecord.bookAuthor
|
targetRecord.bookAuthor
|
||||||
) ?: targetRecord
|
) ?: targetRecord
|
||||||
|
val targetBookUrl = target.bookUrl ?: source.bookUrl
|
||||||
|
|
||||||
dao.insert(
|
dao.insert(
|
||||||
target.copy(
|
target.copy(
|
||||||
|
bookUrl = targetBookUrl,
|
||||||
readTime = target.readTime + source.readTime,
|
readTime = target.readTime + source.readTime,
|
||||||
lastRead = max(target.lastRead, source.lastRead)
|
lastRead = max(target.lastRead, source.lastRead)
|
||||||
)
|
)
|
||||||
@@ -311,12 +376,14 @@ class ReadRecordRepository(
|
|||||||
if (existingTargetDetail == null) {
|
if (existingTargetDetail == null) {
|
||||||
dao.insertDetail(
|
dao.insertDetail(
|
||||||
detail.copy(
|
detail.copy(
|
||||||
bookAuthor = targetRecord.bookAuthor
|
bookAuthor = targetRecord.bookAuthor,
|
||||||
|
bookUrl = targetBookUrl,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
dao.insertDetail(
|
dao.insertDetail(
|
||||||
existingTargetDetail.copy(
|
existingTargetDetail.copy(
|
||||||
|
bookUrl = targetBookUrl ?: existingTargetDetail.bookUrl,
|
||||||
readTime = existingTargetDetail.readTime + detail.readTime,
|
readTime = existingTargetDetail.readTime + detail.readTime,
|
||||||
readWords = existingTargetDetail.readWords + detail.readWords,
|
readWords = existingTargetDetail.readWords + detail.readWords,
|
||||||
firstReadTime = min(existingTargetDetail.firstReadTime, detail.firstReadTime),
|
firstReadTime = min(existingTargetDetail.firstReadTime, detail.firstReadTime),
|
||||||
@@ -333,11 +400,22 @@ class ReadRecordRepository(
|
|||||||
sourceRecord.bookAuthor
|
sourceRecord.bookAuthor
|
||||||
)
|
)
|
||||||
sourceSessions.forEach { session ->
|
sourceSessions.forEach { session ->
|
||||||
dao.updateSession(session.copy(bookAuthor = targetRecord.bookAuthor))
|
dao.updateSession(
|
||||||
|
session.copy(
|
||||||
|
bookAuthor = targetRecord.bookAuthor,
|
||||||
|
bookUrl = targetBookUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dao.deleteReadRecord(source)
|
dao.deleteReadRecord(source)
|
||||||
updateReadRecordTotal(targetRecord.deviceId, targetRecord.bookName, targetRecord.bookAuthor)
|
updateReadRecordTotal(targetRecord.deviceId, targetRecord.bookName, targetRecord.bookAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<ReadRecordSession>.resolvedBookUrl(): String? =
|
||||||
|
asSequence()
|
||||||
|
.mapNotNull { it.bookUrl?.takeIf(String::isNotBlank) }
|
||||||
|
.distinct()
|
||||||
|
.singleOrNull()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class ReadSettingsRepository(
|
|||||||
textBottomJustify = this[Keys.TextBottomJustify] ?: true,
|
textBottomJustify = this[Keys.TextBottomJustify] ?: true,
|
||||||
adaptSpecialStyle = this[Keys.AdaptSpecialStyle] ?: true,
|
adaptSpecialStyle = this[Keys.AdaptSpecialStyle] ?: true,
|
||||||
useZhLayout = this[Keys.UseZhLayout] ?: false,
|
useZhLayout = this[Keys.UseZhLayout] ?: false,
|
||||||
showBrightnessView = this[Keys.ShowBrightnessView] ?: "1",
|
showBrightnessView = getStringCompat(PreferKey.showBrightnessView, "1"),
|
||||||
brightnessVwPos = this[Keys.BrightnessVwPos] ?: "1",
|
brightnessVwPos = this[Keys.BrightnessVwPos] ?: "1",
|
||||||
readBrightness = this[Keys.ReadBrightness] ?: 100,
|
readBrightness = this[Keys.ReadBrightness] ?: 100,
|
||||||
brightnessAuto = this[Keys.BrightnessAuto] ?: false,
|
brightnessAuto = this[Keys.BrightnessAuto] ?: false,
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun normalizeLegacyPreferenceValue(key: String, value: Any): Any =
|
||||||
|
when {
|
||||||
|
key == PreferKey.showBrightnessView && value is Boolean -> if (value) "1" else "0"
|
||||||
|
else -> value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Preferences.getStringCompat(key: String, defaultValue: String): String {
|
||||||
|
val value = asMap().entries.firstOrNull { it.key.name == key }?.value ?: return defaultValue
|
||||||
|
return normalizeLegacyPreferenceValue(key, value) as? String ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置仓储
|
* 设置仓储
|
||||||
* 以 DataStore 为唯一写入源,读取以 DataStore 为准。
|
* 以 DataStore 为唯一写入源,读取以 DataStore 为准。
|
||||||
@@ -110,15 +121,15 @@ class SettingsRepository(private val context: Context) {
|
|||||||
suspend fun batchPutFromMap(map: Map<String, *>) {
|
suspend fun batchPutFromMap(map: Map<String, *>) {
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
map.forEach { (key, value) ->
|
map.forEach { (key, value) ->
|
||||||
when (value) {
|
when (val normalizedValue = value?.let { normalizeLegacyPreferenceValue(key, it) }) {
|
||||||
is String -> preferences[stringPreferencesKey(key)] = value
|
is String -> preferences[stringPreferencesKey(key)] = normalizedValue
|
||||||
is Int -> preferences[intPreferencesKey(key)] = value
|
is Int -> preferences[intPreferencesKey(key)] = normalizedValue
|
||||||
is Boolean -> preferences[booleanPreferencesKey(key)] = value
|
is Boolean -> preferences[booleanPreferencesKey(key)] = normalizedValue
|
||||||
is Long -> preferences[longPreferencesKey(key)] = value
|
is Long -> preferences[longPreferencesKey(key)] = normalizedValue
|
||||||
is Float -> preferences[floatPreferencesKey(key)] = value
|
is Float -> preferences[floatPreferencesKey(key)] = normalizedValue
|
||||||
is Set<*> -> {
|
is Set<*> -> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
preferences[stringSetPreferencesKey(key)] = value as Set<String>
|
preferences[stringSetPreferencesKey(key)] = normalizedValue as Set<String>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.legado.app.data.repository.BackupRestoreRepository
|
|||||||
import io.legado.app.data.repository.BookCacheCleanupRepository
|
import io.legado.app.data.repository.BookCacheCleanupRepository
|
||||||
import io.legado.app.data.repository.BookDomainRepositoryImpl
|
import io.legado.app.data.repository.BookDomainRepositoryImpl
|
||||||
import io.legado.app.data.repository.BookGroupRepository
|
import io.legado.app.data.repository.BookGroupRepository
|
||||||
|
import io.legado.app.data.repository.BookPrivacyRepository
|
||||||
import io.legado.app.data.repository.BookRepository
|
import io.legado.app.data.repository.BookRepository
|
||||||
import io.legado.app.data.repository.BookSourceCallbackRepository
|
import io.legado.app.data.repository.BookSourceCallbackRepository
|
||||||
import io.legado.app.data.repository.BookSourceRepository
|
import io.legado.app.data.repository.BookSourceRepository
|
||||||
@@ -179,8 +180,11 @@ val appModule = module {
|
|||||||
single { get<AppDatabase>().bookGroupDao }
|
single { get<AppDatabase>().bookGroupDao }
|
||||||
single { get<AppDatabase>().bookSourceDao }
|
single { get<AppDatabase>().bookSourceDao }
|
||||||
|
|
||||||
|
singleOf(::BookPrivacyRepository)
|
||||||
singleOf(::ReadRecordRepository)
|
singleOf(::ReadRecordRepository)
|
||||||
single<HomeDashboardGateway> { HomeDashboardRepository(get(), get()) }
|
single<HomeDashboardGateway> {
|
||||||
|
HomeDashboardRepository(get(), get(), get(), get())
|
||||||
|
}
|
||||||
singleOf(::BookRepository)
|
singleOf(::BookRepository)
|
||||||
singleOf(::BookGroupRepository)
|
singleOf(::BookGroupRepository)
|
||||||
singleOf(::BookSourceRepository)
|
singleOf(::BookSourceRepository)
|
||||||
@@ -230,7 +234,9 @@ val appModule = module {
|
|||||||
single<AiChatGateway> { AiChatRepository(get()) }
|
single<AiChatGateway> { AiChatRepository(get()) }
|
||||||
single<AiMemoryGateway> { AiMemoryRepository(get()) }
|
single<AiMemoryGateway> { AiMemoryRepository(get()) }
|
||||||
single<AiTextGateway> { AiTextRepositoryImpl() }
|
single<AiTextGateway> { AiTextRepositoryImpl() }
|
||||||
single<AiToolGateway> { AiToolRepository(get(), get(), get(), get(), get(), get()) }
|
single<AiToolGateway> {
|
||||||
|
AiToolRepository(get(), get(), get(), get(), get(), get(), get())
|
||||||
|
}
|
||||||
single<AppStartupGateway> { AppStartupRepository(get()) }
|
single<AppStartupGateway> { AppStartupRepository(get()) }
|
||||||
single<BackupRestoreGateway> { BackupRestoreRepository() }
|
single<BackupRestoreGateway> { BackupRestoreRepository() }
|
||||||
single<BookCacheDownloadGateway> { CacheBookDownloadRepository(get()) }
|
single<BookCacheDownloadGateway> { CacheBookDownloadRepository(get()) }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.legado.app.constant.AppLog
|
|||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.dao.BookChapterDao
|
import io.legado.app.data.dao.BookChapterDao
|
||||||
import io.legado.app.data.dao.BookDao
|
import io.legado.app.data.dao.BookDao
|
||||||
|
import io.legado.app.data.dao.ReadRecordDao
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
@@ -68,6 +69,7 @@ enum class BatchChangeSourcePreviewStatus {
|
|||||||
class ChangeBookSourceUseCase(
|
class ChangeBookSourceUseCase(
|
||||||
private val bookDao: BookDao,
|
private val bookDao: BookDao,
|
||||||
private val bookChapterDao: BookChapterDao,
|
private val bookChapterDao: BookChapterDao,
|
||||||
|
private val readRecordDao: ReadRecordDao,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun applyMigration(
|
fun applyMigration(
|
||||||
@@ -97,6 +99,7 @@ class ChangeBookSourceUseCase(
|
|||||||
bookChapterDao.delByBook(oldBook.bookUrl)
|
bookChapterDao.delByBook(oldBook.bookUrl)
|
||||||
bookDao.delete(oldBook)
|
bookDao.delete(oldBook)
|
||||||
bookDao.insert(newBook)
|
bookDao.insert(newBook)
|
||||||
|
readRecordDao.replaceBookUrl(oldBookUrl, newBook.bookUrl)
|
||||||
if (options.migrateChapters) {
|
if (options.migrateChapters) {
|
||||||
bookChapterDao.insert(*chapters.toTypedArray())
|
bookChapterDao.insert(*chapters.toTypedArray())
|
||||||
ReadBook.onChapterListUpdated(newBook)
|
ReadBook.onChapterListUpdated(newBook)
|
||||||
|
|||||||
@@ -214,17 +214,28 @@ object Restore : KoinComponent {
|
|||||||
}
|
}
|
||||||
fileToListT<ReadRecord>(path, "readRecord.json")?.let {
|
fileToListT<ReadRecord>(path, "readRecord.json")?.let {
|
||||||
it.forEach { readRecord ->
|
it.forEach { readRecord ->
|
||||||
if (readRecord.deviceId != androidId) {
|
val restoredRecord = readRecord.copy(
|
||||||
|
bookUrl = resolveBookUrl(
|
||||||
|
readRecord.bookUrl,
|
||||||
|
readRecord.bookName,
|
||||||
|
readRecord.bookAuthor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (restoredRecord.deviceId != androidId) {
|
||||||
try {
|
try {
|
||||||
appDb.readRecordDao.insert(readRecord)
|
appDb.readRecordDao.insert(restoredRecord)
|
||||||
} catch (_: SQLiteConstraintException) {
|
} catch (_: SQLiteConstraintException) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val time = appDb.readRecordDao
|
val time = appDb.readRecordDao
|
||||||
.getReadTime(readRecord.deviceId, readRecord.bookName, readRecord.bookAuthor)
|
.getReadTime(
|
||||||
if (time == null || time < readRecord.readTime) {
|
restoredRecord.deviceId,
|
||||||
|
restoredRecord.bookName,
|
||||||
|
restoredRecord.bookAuthor,
|
||||||
|
)
|
||||||
|
if (time == null || time < restoredRecord.readTime) {
|
||||||
try {
|
try {
|
||||||
appDb.readRecordDao.insert(readRecord)
|
appDb.readRecordDao.insert(restoredRecord)
|
||||||
} catch (_: SQLiteConstraintException) {
|
} catch (_: SQLiteConstraintException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +245,15 @@ object Restore : KoinComponent {
|
|||||||
fileToListT<ReadRecordDetail>(path, "readRecordDetail.json")?.let {
|
fileToListT<ReadRecordDetail>(path, "readRecordDetail.json")?.let {
|
||||||
it.forEach { detail ->
|
it.forEach { detail ->
|
||||||
try {
|
try {
|
||||||
appDb.readRecordDao.insertDetail(detail)
|
appDb.readRecordDao.insertDetail(
|
||||||
|
detail.copy(
|
||||||
|
bookUrl = resolveBookUrl(
|
||||||
|
detail.bookUrl,
|
||||||
|
detail.bookName,
|
||||||
|
detail.bookAuthor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (_: SQLiteConstraintException) {
|
} catch (_: SQLiteConstraintException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +261,15 @@ object Restore : KoinComponent {
|
|||||||
fileToListT<ReadRecordSession>(path, "readRecordSession.json")?.let {
|
fileToListT<ReadRecordSession>(path, "readRecordSession.json")?.let {
|
||||||
it.forEach { session ->
|
it.forEach { session ->
|
||||||
try {
|
try {
|
||||||
appDb.readRecordDao.insertSession(session)
|
appDb.readRecordDao.insertSession(
|
||||||
|
session.copy(
|
||||||
|
bookUrl = resolveBookUrl(
|
||||||
|
session.bookUrl,
|
||||||
|
session.bookName,
|
||||||
|
session.bookAuthor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (_: SQLiteConstraintException) {
|
} catch (_: SQLiteConstraintException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +437,15 @@ object Restore : KoinComponent {
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveBookUrl(
|
||||||
|
existingBookUrl: String?,
|
||||||
|
bookName: String,
|
||||||
|
bookAuthor: String,
|
||||||
|
): String? {
|
||||||
|
if (!existingBookUrl.isNullOrBlank()) return existingBookUrl
|
||||||
|
return appDb.readRecordDao.findMatchingBookUrls(bookName, bookAuthor).singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
|
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
|
||||||
try {
|
try {
|
||||||
val file = File(path, fileName)
|
val file = File(path, fileName)
|
||||||
|
|||||||
@@ -68,11 +68,12 @@ data class Asset(
|
|||||||
val downloadCount: Int,
|
val downloadCount: Int,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val state: String,
|
val state: String?,
|
||||||
val url: String
|
val url: String
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean
|
val isValid: Boolean
|
||||||
get() = (contentType == "application/vnd.android.package-archive") && (state == "uploaded")
|
get() = name.endsWith(".apk", ignoreCase = true) &&
|
||||||
|
(state == null || state == "uploaded")
|
||||||
|
|
||||||
fun assetToAppReleaseInfo(preRelease: Boolean, note: String, version: String): AppReleaseInfo {
|
fun assetToAppReleaseInfo(preRelease: Boolean, note: String, version: String): AppReleaseInfo {
|
||||||
val instant = Instant.parse(createdAt)
|
val instant = Instant.parse(createdAt)
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
|
|
||||||
object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
||||||
|
|
||||||
|
private const val RELEASES_API =
|
||||||
|
"https://gitea.yamby.cn/api/v1/repos/yusheng/QieKan-3.0/releases"
|
||||||
|
|
||||||
private val checkVariant: AppVariant
|
private val checkVariant: AppVariant
|
||||||
get() = when (AppConfig.updateToVariant) {
|
get() = when (AppConfig.updateToVariant) {
|
||||||
"official_version" -> AppVariant.OFFICIAL
|
"official_version" -> AppVariant.OFFICIAL
|
||||||
@@ -24,9 +27,9 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
|
|
||||||
private suspend fun getLatestRelease(): List<AppReleaseInfo> {
|
private suspend fun getLatestRelease(): List<AppReleaseInfo> {
|
||||||
val url = if (checkVariant == AppVariant.OFFICIAL)
|
val url = if (checkVariant == AppVariant.OFFICIAL)
|
||||||
"https://api.github.com/repos/HapeLee/legado-with-MD3/releases/latest"
|
"$RELEASES_API/latest"
|
||||||
else
|
else
|
||||||
"https://api.github.com/repos/HapeLee/legado-with-MD3/releases"
|
RELEASES_API
|
||||||
|
|
||||||
val res = okHttpClient.newCallResponse { url(url) }
|
val res = okHttpClient.newCallResponse { url(url) }
|
||||||
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
|
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
|
||||||
@@ -65,7 +68,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getReleaseByTag(tag: String): AppUpdate.UpdateInfo? {
|
suspend fun getReleaseByTag(tag: String): AppUpdate.UpdateInfo? {
|
||||||
val url = "https://api.github.com/repos/HapeLee/legado-with-MD3/releases/tags/$tag"
|
val url = "$RELEASES_API/tags/$tag"
|
||||||
val res = okHttpClient.newCallResponse { url(url) }
|
val res = okHttpClient.newCallResponse { url(url) }
|
||||||
if (!res.isSuccessful) return null
|
if (!res.isSuccessful) return null
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
ReadBook.book = book
|
ReadBook.book = book
|
||||||
readRecord.bookName = book.name
|
readRecord.bookName = book.name
|
||||||
readRecord.bookAuthor = book.author
|
readRecord.bookAuthor = book.author
|
||||||
|
readRecord.bookUrl = book.bookUrl
|
||||||
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
|
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
|
||||||
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||||
simulatedChapterSize = if (book.readSimulating()) {
|
simulatedChapterSize = if (book.readSimulating()) {
|
||||||
@@ -330,6 +331,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
deviceId = "",
|
deviceId = "",
|
||||||
bookName = currentBookName,
|
bookName = currentBookName,
|
||||||
bookAuthor = currentBookAuthor,
|
bookAuthor = currentBookAuthor,
|
||||||
|
bookUrl = book?.bookUrl,
|
||||||
startTime = readStartTime,
|
startTime = readStartTime,
|
||||||
endTime = readStartTime,
|
endTime = readStartTime,
|
||||||
words = durChapterIndex.toLong()
|
words = durChapterIndex.toLong()
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
|
|||||||
ReadManga.book = book
|
ReadManga.book = book
|
||||||
readRecord.bookName = book.name
|
readRecord.bookName = book.name
|
||||||
readRecord.bookAuthor = book.author
|
readRecord.bookAuthor = book.author
|
||||||
|
readRecord.bookUrl = book.bookUrl
|
||||||
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
|
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
|
||||||
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||||
simulatedChapterSize = if (book.readSimulating()) {
|
simulatedChapterSize = if (book.readSimulating()) {
|
||||||
@@ -162,6 +163,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
|
|||||||
deviceId = "",
|
deviceId = "",
|
||||||
bookName = currentBookName,
|
bookName = currentBookName,
|
||||||
bookAuthor = currentBookAuthor,
|
bookAuthor = currentBookAuthor,
|
||||||
|
bookUrl = book?.bookUrl,
|
||||||
startTime = readStartTime,
|
startTime = readStartTime,
|
||||||
endTime = readStartTime,
|
endTime = readStartTime,
|
||||||
words = durChapterIndex.toLong()
|
words = durChapterIndex.toLong()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ fun AboutScreen(
|
|||||||
state: AboutUiState,
|
state: AboutUiState,
|
||||||
onIntent: (AboutIntent) -> Unit,
|
onIntent: (AboutIntent) -> Unit,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
versionName: String = appInfo.versionName,
|
versionName: String = "${appInfo.versionName} (${appInfo.versionCode})",
|
||||||
) {
|
) {
|
||||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
||||||
MiuixAboutScreen(
|
MiuixAboutScreen(
|
||||||
@@ -145,13 +145,13 @@ private fun MaterialAboutScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
|
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_web_outline),
|
painter = painterResource(R.drawable.ic_web_outline),
|
||||||
contentDescription = stringResource(R.string.back)
|
contentDescription = stringResource(R.string.back)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
|
FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_github),
|
painter = painterResource(R.drawable.ic_github),
|
||||||
contentDescription = stringResource(R.string.back)
|
contentDescription = stringResource(R.string.back)
|
||||||
@@ -172,7 +172,7 @@ private fun MaterialAboutScreen(
|
|||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.contributors),
|
title = stringResource(R.string.contributors),
|
||||||
onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }
|
onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingItemWithDivider {
|
SettingItemWithDivider {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ fun MiuixAboutScreen(
|
|||||||
state: AboutUiState,
|
state: AboutUiState,
|
||||||
onIntent: (AboutIntent) -> Unit,
|
onIntent: (AboutIntent) -> Unit,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
versionName: String = appInfo.versionName,
|
versionName: String = "${appInfo.versionName} (${appInfo.versionCode})",
|
||||||
) {
|
) {
|
||||||
val topAppBarScrollBehavior = MiuixScrollBehavior()
|
val topAppBarScrollBehavior = MiuixScrollBehavior()
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
@@ -395,7 +395,7 @@ private fun AboutContent(
|
|||||||
endActions = {
|
endActions = {
|
||||||
ValueText("GitHub")
|
ValueText("GitHub")
|
||||||
},
|
},
|
||||||
onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) },
|
onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ fun GroupEditContent(
|
|||||||
val sortEntryValues = remember(sortOptions) {
|
val sortEntryValues = remember(sortOptions) {
|
||||||
Array(sortOptions.size) { (it - 1).toString() }
|
Array(sortOptions.size) { (it - 1).toString() }
|
||||||
}
|
}
|
||||||
val canSetPrivate = group == null || group.groupId > 0
|
val canSetPrivate = group == null || group.groupId > 0 || group.groupId == Long.MIN_VALUE
|
||||||
|
|
||||||
val selectImage = rememberLauncherForActivityResult(SelectImageContract()) { result ->
|
val selectImage = rememberLauncherForActivityResult(SelectImageContract()) { result ->
|
||||||
result.uri?.let { uri ->
|
result.uri?.let { uri ->
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ReadRecordOverviewViewModel(
|
|||||||
_referenceDate,
|
_referenceDate,
|
||||||
repository.getAllRecordDetails(""),
|
repository.getAllRecordDetails(""),
|
||||||
repository.getLatestReadRecords(""),
|
repository.getLatestReadRecords(""),
|
||||||
bookRepository.getAllBooks()
|
bookRepository.getVisibleBooks()
|
||||||
) { period, refDate, details, latestRecords, allBooks ->
|
) { period, refDate, details, latestRecords, allBooks ->
|
||||||
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
|
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.data.repository.BookRepository
|
||||||
import io.legado.app.model.Download
|
import io.legado.app.model.Download
|
||||||
import io.legado.app.ui.about.AboutEffect
|
import io.legado.app.ui.about.AboutEffect
|
||||||
import io.legado.app.ui.about.AboutScreen
|
import io.legado.app.ui.about.AboutScreen
|
||||||
@@ -74,11 +75,10 @@ import io.legado.app.utils.startActivityForBook
|
|||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.toggleSystemBar
|
import io.legado.app.utils.toggleSystemBar
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@@ -559,13 +559,12 @@ fun MainActivity.mainEntryProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry<MainRouteReadRecord> {
|
entry<MainRouteReadRecord> {
|
||||||
|
val bookRepository = koinInject<BookRepository>()
|
||||||
ReadRecordScreen(
|
ReadRecordScreen(
|
||||||
onBackClick = { onNavigateBack() },
|
onBackClick = { onNavigateBack() },
|
||||||
onBookClick = { name, author ->
|
onBookClick = { name, author ->
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val book = withContext(IO) {
|
val book = bookRepository.getVisibleBook(name, author)
|
||||||
io.legado.app.data.appDb.bookDao.getBook(name, author)
|
|
||||||
}
|
|
||||||
if (book != null) this@mainEntryProvider.startActivityForBook(book)
|
if (book != null) this@mainEntryProvider.startActivityForBook(book)
|
||||||
else {
|
else {
|
||||||
onNavigateToRoute(MainRouteSearch(key = name))
|
onNavigateToRoute(MainRouteSearch(key = name))
|
||||||
@@ -579,13 +578,12 @@ fun MainActivity.mainEntryProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry<MainRouteReadRecordOverview> {
|
entry<MainRouteReadRecordOverview> {
|
||||||
|
val bookRepository = koinInject<BookRepository>()
|
||||||
ReadRecordOverviewScreen(
|
ReadRecordOverviewScreen(
|
||||||
onBackClick = { onNavigateBack() },
|
onBackClick = { onNavigateBack() },
|
||||||
onBookClick = { name, author ->
|
onBookClick = { name, author ->
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val book = withContext(IO) {
|
val book = bookRepository.getVisibleBook(name, author)
|
||||||
io.legado.app.data.appDb.bookDao.getBook(name, author)
|
|
||||||
}
|
|
||||||
if (book != null) this@mainEntryProvider.startActivityForBook(book)
|
if (book != null) this@mainEntryProvider.startActivityForBook(book)
|
||||||
else {
|
else {
|
||||||
onNavigateToRoute(MainRouteSearch(key = name))
|
onNavigateToRoute(MainRouteSearch(key = name))
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import io.legado.app.ui.widget.components.icon.AppIcon
|
|||||||
import io.legado.app.ui.widget.components.image.cover.BookshelfCover
|
import io.legado.app.ui.widget.components.image.cover.BookshelfCover
|
||||||
import io.legado.app.ui.widget.components.image.cover.CoilBookCover
|
import io.legado.app.ui.widget.components.image.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
|
||||||
import io.legado.app.utils.toTimeAgo
|
import io.legado.app.utils.toTimeAgo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +84,7 @@ fun BookshelfItem(
|
|||||||
titleMaxLines: Int = 2,
|
titleMaxLines: Int = 2,
|
||||||
coverShadow: Boolean = false,
|
coverShadow: Boolean = false,
|
||||||
titleColor: Color? = null,
|
titleColor: Color? = null,
|
||||||
|
titleHidden: Boolean = false,
|
||||||
descAnnotated: AnnotatedString? = null,
|
descAnnotated: AnnotatedString? = null,
|
||||||
coverWidth: Int = 84,
|
coverWidth: Int = 84,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
@@ -151,6 +153,7 @@ fun BookshelfItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.hiddenAttributeUnderline(titleHidden, Color.White)
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
@@ -173,6 +176,10 @@ fun BookshelfItem(
|
|||||||
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
|
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.hiddenAttributeUnderline(
|
||||||
|
titleHidden,
|
||||||
|
LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
.padding(start = 4.dp, end = 4.dp, bottom = 4.dp)
|
.padding(start = 4.dp, end = 4.dp, bottom = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -237,7 +244,12 @@ fun BookshelfItem(
|
|||||||
},
|
},
|
||||||
maxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
maxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.hiddenAttributeUnderline(
|
||||||
|
titleHidden,
|
||||||
|
LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
)
|
)
|
||||||
titleEnd?.let {
|
titleEnd?.let {
|
||||||
Box(modifier = Modifier.padding(top = 4.dp, start = 4.dp)) {
|
Box(modifier = Modifier.padding(top = 4.dp, start = 4.dp)) {
|
||||||
@@ -436,6 +448,7 @@ fun BookGroupItemGrid(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
|
titleHidden = group.isPrivate,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
titleSmallFont = titleSmallFont,
|
titleSmallFont = titleSmallFont,
|
||||||
titleCenter = titleCenter,
|
titleCenter = titleCenter,
|
||||||
@@ -491,6 +504,7 @@ fun BookGroupItemList(
|
|||||||
isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact,
|
isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact,
|
||||||
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
|
titleHidden = group.isPrivate,
|
||||||
subTitle = countText,
|
subTitle = countText,
|
||||||
descAnnotated = descAnnotated,
|
descAnnotated = descAnnotated,
|
||||||
titleSmallFont = titleSmallFont,
|
titleSmallFont = titleSmallFont,
|
||||||
@@ -549,6 +563,10 @@ fun BookGroupItemHorizontalCovers(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
|
.hiddenAttributeUnderline(
|
||||||
|
group.isPrivate,
|
||||||
|
LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (countText != null) {
|
if (countText != null) {
|
||||||
AppText(
|
AppText(
|
||||||
@@ -689,6 +707,7 @@ fun BookItem(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
title = book.name,
|
title = book.name,
|
||||||
|
titleHidden = bookUi.isHidden,
|
||||||
subTitle = if (layoutMode == 0 && isCompact) {
|
subTitle = if (layoutMode == 0 && isCompact) {
|
||||||
stringResource(R.string.author_read, book.author, unreadCount)
|
stringResource(R.string.author_read, book.author, unreadCount)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ data class BookShelfItem(
|
|||||||
/**
|
/**
|
||||||
* 将 DTO 转换为专为 Compose 设计的 UI 状态
|
* 将 DTO 转换为专为 Compose 设计的 UI 状态
|
||||||
*/
|
*/
|
||||||
fun toUiItem(): BookUiItem {
|
fun toUiItem(privateGroupMask: Long = 0L): BookUiItem {
|
||||||
val tagList = mutableListOf<String>()
|
val tagList = mutableListOf<String>()
|
||||||
kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }?.let {
|
kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }?.let {
|
||||||
tagList.addAll(it)
|
tagList.addAll(it)
|
||||||
@@ -61,7 +61,8 @@ data class BookShelfItem(
|
|||||||
|
|
||||||
return BookUiItem(
|
return BookUiItem(
|
||||||
book = this,
|
book = this,
|
||||||
displayTags = tagList.toImmutableList()
|
displayTags = tagList.toImmutableList(),
|
||||||
|
isHidden = privateGroupMask != 0L && (group and privateGroupMask) != 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,8 @@ data class BookShelfItem(
|
|||||||
@Stable
|
@Stable
|
||||||
data class BookUiItem(
|
data class BookUiItem(
|
||||||
val book: BookShelfItem,
|
val book: BookShelfItem,
|
||||||
val displayTags: ImmutableList<String>
|
val displayTags: ImmutableList<String>,
|
||||||
|
val isHidden: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun matches(key: String): Boolean {
|
fun matches(key: String): Boolean {
|
||||||
return book.name.contains(key, true) ||
|
return book.name.contains(key, true) ||
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import androidx.compose.material.icons.filled.Refresh
|
|||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material.icons.filled.SelectAll
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
import androidx.compose.material.icons.filled.UploadFile
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material.icons.filled.Wifi
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
import androidx.compose.material.icons.outlined.ViewCarousel
|
import androidx.compose.material.icons.outlined.ViewCarousel
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -73,6 +74,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -238,7 +240,9 @@ fun BookshelfScreen(
|
|||||||
)
|
)
|
||||||
val folderGridState = rememberLazyGridState()
|
val folderGridState = rememberLazyGridState()
|
||||||
val standaloneSearchGridState = rememberLazyGridState()
|
val standaloneSearchGridState = rememberLazyGridState()
|
||||||
val groupGridStates = mutableMapOf<Long, LazyGridState>()
|
// Pager may keep composing an old keyed page briefly after the visible group list changes.
|
||||||
|
// Retain states for removed groups until those pages are disposed to avoid a missing map key.
|
||||||
|
val groupGridStates = remember { mutableMapOf<Long, LazyGridState>() }
|
||||||
uiState.groups.forEach { group ->
|
uiState.groups.forEach { group ->
|
||||||
key(group.groupId) {
|
key(group.groupId) {
|
||||||
groupGridStates[group.groupId] = rememberLazyGridState()
|
groupGridStates[group.groupId] = rememberLazyGridState()
|
||||||
@@ -507,6 +511,19 @@ fun BookshelfScreen(
|
|||||||
},
|
},
|
||||||
leadingIcon = { Icon(Icons.Outlined.ViewCarousel, null) }
|
leadingIcon = { Icon(Icons.Outlined.ViewCarousel, null) }
|
||||||
)
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.privacy_mode),
|
||||||
|
onClick = {
|
||||||
|
viewModel.setPrivacyMode(!uiState.privacyModeEnabled)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.VisibilityOff, null) },
|
||||||
|
trailingIcon = {
|
||||||
|
Switch(
|
||||||
|
checked = uiState.privacyModeEnabled,
|
||||||
|
onCheckedChange = viewModel::setPrivacyMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = stringResource(R.string.add_url),
|
text = stringResource(R.string.add_url),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -578,6 +595,9 @@ fun BookshelfScreen(
|
|||||||
|
|
||||||
AppTabRow(
|
AppTabRow(
|
||||||
tabTitles = tabTitles,
|
tabTitles = tabTitles,
|
||||||
|
hiddenTabIndices = uiState.groups.mapIndexedNotNull { index, group ->
|
||||||
|
index.takeIf { group.isPrivate }
|
||||||
|
}.toSet(),
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
onTabSelected = { index ->
|
onTabSelected = { index ->
|
||||||
scope.launch { pagerState.animateScrollToPage(index) }
|
scope.launch { pagerState.animateScrollToPage(index) }
|
||||||
@@ -608,6 +628,7 @@ fun BookshelfScreen(
|
|||||||
uiState.groups.forEachIndexed { index, group ->
|
uiState.groups.forEachIndexed { index, group ->
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = group.groupName,
|
text = group.groupName,
|
||||||
|
hidden = group.isPrivate,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (uiState.isSearch) {
|
if (uiState.isSearch) {
|
||||||
viewModel.changeGroup(group.groupId)
|
viewModel.changeGroup(group.groupId)
|
||||||
@@ -641,7 +662,9 @@ fun BookshelfScreen(
|
|||||||
it.groupId == BookGroup.IdAll
|
it.groupId == BookGroup.IdAll
|
||||||
}
|
}
|
||||||
val hiddenGroups = uiState.allGroups.filter {
|
val hiddenGroups = uiState.allGroups.filter {
|
||||||
!it.show && it.groupId != BookGroup.IdAll
|
!it.show &&
|
||||||
|
it.groupId != BookGroup.IdAll &&
|
||||||
|
(!uiState.privacyModeEnabled || !it.isPrivate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allGroup != null || hiddenGroups.isNotEmpty()) {
|
if (allGroup != null || hiddenGroups.isNotEmpty()) {
|
||||||
@@ -656,6 +679,7 @@ fun BookshelfScreen(
|
|||||||
allGroup?.let { group ->
|
allGroup?.let { group ->
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = group.groupName,
|
text = group.groupName,
|
||||||
|
hidden = group.isPrivate,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.changeGroup(group.groupId)
|
viewModel.changeGroup(group.groupId)
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -675,6 +699,7 @@ fun BookshelfScreen(
|
|||||||
hiddenGroups.forEach { group ->
|
hiddenGroups.forEach { group ->
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = group.groupName,
|
text = group.groupName,
|
||||||
|
hidden = group.isPrivate,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.changeGroup(group.groupId)
|
viewModel.changeGroup(group.groupId)
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -999,6 +1024,7 @@ fun BookshelfScreen(
|
|||||||
uiState.groups.forEach { group ->
|
uiState.groups.forEach { group ->
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = group.groupName,
|
text = group.groupName,
|
||||||
|
hidden = group.isPrivate,
|
||||||
onClick = {
|
onClick = {
|
||||||
val targetIndex =
|
val targetIndex =
|
||||||
uiState.groups.indexOfFirst { it.groupId == group.groupId }
|
uiState.groups.indexOfFirst { it.groupId == group.groupId }
|
||||||
@@ -1140,7 +1166,10 @@ private fun BookshelfOverlays(
|
|||||||
|
|
||||||
GroupSelectSheet(
|
GroupSelectSheet(
|
||||||
show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
|
show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
|
||||||
groups = groups.filter { it.groupId > 0 },
|
groups = groups.filter {
|
||||||
|
(it.groupId > 0 || it.groupId == Long.MIN_VALUE) &&
|
||||||
|
(!uiState.privacyModeEnabled || !it.isPrivate)
|
||||||
|
},
|
||||||
currentGroupId = 0L,
|
currentGroupId = 0L,
|
||||||
onDismissRequest = { viewModel.dismissOverlay() },
|
onDismissRequest = { viewModel.dismissOverlay() },
|
||||||
onConfirm = { groupId ->
|
onConfirm = { groupId ->
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ data class BookshelfUiState(
|
|||||||
val bookGroupStyle: Int = 0,
|
val bookGroupStyle: Int = 0,
|
||||||
val bookshelfSort: Int = 0,
|
val bookshelfSort: Int = 0,
|
||||||
val bookshelfSortOrder: Int = 1,
|
val bookshelfSortOrder: Int = 1,
|
||||||
|
val privacyModeEnabled: Boolean = true,
|
||||||
val title: String = "",
|
val title: String = "",
|
||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val currentGroupName: String? = null,
|
val currentGroupName: String? = null,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.legado.app.base.BaseViewModel
|
|||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookGroup
|
import io.legado.app.data.entities.BookGroup
|
||||||
@@ -18,6 +19,7 @@ import io.legado.app.data.repository.BookGroupRepository
|
|||||||
import io.legado.app.data.repository.BookRepository
|
import io.legado.app.data.repository.BookRepository
|
||||||
import io.legado.app.data.repository.BookSourceRepository
|
import io.legado.app.data.repository.BookSourceRepository
|
||||||
import io.legado.app.data.repository.BookshelfRepository
|
import io.legado.app.data.repository.BookshelfRepository
|
||||||
|
import io.legado.app.data.repository.SettingsRepository
|
||||||
import io.legado.app.data.repository.UploadRepository
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.domain.usecase.AddBookUseCase
|
import io.legado.app.domain.usecase.AddBookUseCase
|
||||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||||
@@ -81,6 +83,7 @@ class BookshelfViewModel(
|
|||||||
private val bookGroupRepository: BookGroupRepository,
|
private val bookGroupRepository: BookGroupRepository,
|
||||||
private val bookSourceRepository: BookSourceRepository,
|
private val bookSourceRepository: BookSourceRepository,
|
||||||
private val bookshelfRepository: BookshelfRepository,
|
private val bookshelfRepository: BookshelfRepository,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
private val uploadRepository: UploadRepository,
|
private val uploadRepository: UploadRepository,
|
||||||
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
|
||||||
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase,
|
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase,
|
||||||
@@ -110,6 +113,13 @@ class BookshelfViewModel(
|
|||||||
val sortOrder: Int
|
val sortOrder: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class GroupBooksConfig(
|
||||||
|
val groups: List<BookGroup>,
|
||||||
|
val sortConfig: BookshelfSortConfig,
|
||||||
|
val privacyModeEnabled: Boolean,
|
||||||
|
val privateGroupMask: Long
|
||||||
|
)
|
||||||
|
|
||||||
private fun readSortConfig() = BookshelfSortConfig(
|
private fun readSortConfig() = BookshelfSortConfig(
|
||||||
sort = BookshelfConfig.bookshelfSort,
|
sort = BookshelfConfig.bookshelfSort,
|
||||||
sortOrder = BookshelfConfig.bookshelfSortOrder
|
sortOrder = BookshelfConfig.bookshelfSortOrder
|
||||||
@@ -142,7 +152,40 @@ class BookshelfViewModel(
|
|||||||
protected val _eventChannel = Channel<BaseRuleEvent>()
|
protected val _eventChannel = Channel<BaseRuleEvent>()
|
||||||
val events = _eventChannel.receiveAsFlow()
|
val events = _eventChannel.receiveAsFlow()
|
||||||
|
|
||||||
val groupsFlow: SharedFlow<List<BookGroup>> = bookGroupRepository.flowShow()
|
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
|
private val privacyModeFlow: StateFlow<Boolean> = settingsRepository
|
||||||
|
.getBoolean(PreferKey.bookshelfPrivacyMode, true)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, true)
|
||||||
|
|
||||||
|
private val privateGroupMaskFlow: StateFlow<Long> = allGroupsFlow
|
||||||
|
.map { groups ->
|
||||||
|
groups.asSequence()
|
||||||
|
.filter {
|
||||||
|
(it.groupId > 0L || it.groupId == Long.MIN_VALUE) && it.isPrivate
|
||||||
|
}
|
||||||
|
.fold(0L) { mask, group -> mask or group.groupId }
|
||||||
|
}
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L)
|
||||||
|
|
||||||
|
private val allBooksIncludingPrivateFlow = bookRepository.flowBookShelfIncludingPrivate()
|
||||||
|
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||||
|
|
||||||
|
private val displayedAllBookCountFlow = combine(
|
||||||
|
bookRepository.flowAllBookShelfCount(),
|
||||||
|
allBooksIncludingPrivateFlow,
|
||||||
|
privacyModeFlow
|
||||||
|
) { publicCount, allBooks, privacyModeEnabled ->
|
||||||
|
if (privacyModeEnabled) publicCount else allBooks.size
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupsFlow: SharedFlow<List<BookGroup>> = combine(
|
||||||
|
bookGroupRepository.flowShow(),
|
||||||
|
privacyModeFlow
|
||||||
|
) { groups, privacyModeEnabled ->
|
||||||
|
if (privacyModeEnabled) groups.filterNot { it.isPrivate } else groups
|
||||||
|
}
|
||||||
.onEach {
|
.onEach {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
isInitialLoadingFlow.value = false
|
isInitialLoadingFlow.value = false
|
||||||
@@ -150,8 +193,21 @@ class BookshelfViewModel(
|
|||||||
}
|
}
|
||||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||||
|
|
||||||
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
|
private fun displayedBooksFlow(
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
groupId: Long,
|
||||||
|
privacyModeEnabled: Boolean
|
||||||
|
): Flow<List<BookShelfItem>> {
|
||||||
|
return if (
|
||||||
|
!privacyModeEnabled &&
|
||||||
|
(groupId == BookGroup.IdAll || groupId > 0L || groupId == Long.MIN_VALUE)
|
||||||
|
) {
|
||||||
|
allBooksIncludingPrivateFlow.map { books ->
|
||||||
|
if (groupId == BookGroup.IdAll) books else books.filter { (it.group and groupId) != 0L }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bookRepository.flowBookShelfByGroup(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class GroupPreviewState(
|
private data class GroupPreviewState(
|
||||||
val previews: ImmutableMap<Long, ImmutableList<BookUiItem>>,
|
val previews: ImmutableMap<Long, ImmutableList<BookUiItem>>,
|
||||||
@@ -180,38 +236,44 @@ class BookshelfViewModel(
|
|||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState())
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val booksFlow: Flow<List<BookUiItem>> = groupIdFlow
|
val booksFlow: Flow<List<BookUiItem>> = combine(
|
||||||
.flatMapLatest { groupId ->
|
groupIdFlow,
|
||||||
|
privacyModeFlow
|
||||||
|
) { groupId, privacyModeEnabled -> groupId to privacyModeEnabled }
|
||||||
|
.flatMapLatest { (groupId, privacyModeEnabled) ->
|
||||||
combine(
|
combine(
|
||||||
bookRepository.flowBookShelfByGroup(groupId),
|
displayedBooksFlow(groupId, privacyModeEnabled),
|
||||||
groupsFlow,
|
allGroupsFlow,
|
||||||
sortConfigFlow
|
sortConfigFlow,
|
||||||
) { list, groups, sortConfig ->
|
privateGroupMaskFlow
|
||||||
|
) { list, groups, sortConfig, privateGroupMask ->
|
||||||
bookshelfRepository.sortBooks(
|
bookshelfRepository.sortBooks(
|
||||||
list,
|
list,
|
||||||
groups.find { it.groupId == groupId },
|
groups.find { it.groupId == groupId },
|
||||||
sortConfig.sort,
|
sortConfig.sort,
|
||||||
sortConfig.sortOrder
|
sortConfig.sortOrder
|
||||||
).map { it.toUiItem() }
|
).map { it.toUiItem(privateGroupMask) }
|
||||||
}
|
}
|
||||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private val allGroupBooksImmutableFlow: Flow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
|
private val allGroupBooksImmutableFlow: Flow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
|
||||||
combine(groupsFlow, sortConfigFlow) { groups, sortConfig ->
|
combine(groupsFlow, sortConfigFlow, privacyModeFlow, privateGroupMaskFlow) {
|
||||||
groups to sortConfig
|
groups, sortConfig, privacyModeEnabled, privateGroupMask ->
|
||||||
}.flatMapLatest { (groups, sortConfig) ->
|
GroupBooksConfig(groups, sortConfig, privacyModeEnabled, privateGroupMask)
|
||||||
|
}.flatMapLatest { config ->
|
||||||
|
val groups = config.groups
|
||||||
if (groups.isEmpty()) {
|
if (groups.isEmpty()) {
|
||||||
flowOf(persistentMapOf())
|
flowOf(persistentMapOf())
|
||||||
} else {
|
} else {
|
||||||
val flows = groups.map { group ->
|
val flows = groups.map { group ->
|
||||||
bookRepository.flowBookShelfByGroup(group.groupId).map { books ->
|
displayedBooksFlow(group.groupId, config.privacyModeEnabled).map { books ->
|
||||||
group.groupId to bookshelfRepository.sortBooks(
|
group.groupId to bookshelfRepository.sortBooks(
|
||||||
books,
|
books,
|
||||||
group,
|
group,
|
||||||
sortConfig.sort,
|
config.sortConfig.sort,
|
||||||
sortConfig.sortOrder
|
config.sortConfig.sortOrder
|
||||||
).map { it.toUiItem() }.toImmutableList()
|
).map { it.toUiItem(config.privateGroupMask) }.toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
combine(flows) { results ->
|
combine(flows) { results ->
|
||||||
@@ -256,7 +318,7 @@ class BookshelfViewModel(
|
|||||||
groupsFlow,
|
groupsFlow,
|
||||||
bookGroupStyleFlow,
|
bookGroupStyleFlow,
|
||||||
bookRepository.flowSystemGroupCounts(),
|
bookRepository.flowSystemGroupCounts(),
|
||||||
bookRepository.flowAllBookShelfCount()
|
displayedAllBookCountFlow
|
||||||
) { groups, bookGroupStyle, systemCounts, totalCount ->
|
) { groups, bookGroupStyle, systemCounts, totalCount ->
|
||||||
DataForPreviews(
|
DataForPreviews(
|
||||||
groups,
|
groups,
|
||||||
@@ -276,7 +338,9 @@ class BookshelfViewModel(
|
|||||||
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
|
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
|
||||||
} else {
|
} else {
|
||||||
val groupFlows = groups.map { group ->
|
val groupFlows = groups.map { group ->
|
||||||
val countFlow: Flow<Int> = if (group.groupId > 0) {
|
val countFlow: Flow<Int> = if (
|
||||||
|
group.groupId > 0L || group.groupId == Long.MIN_VALUE
|
||||||
|
) {
|
||||||
bookRepository.flowUserGroupBookCount(group.groupId)
|
bookRepository.flowUserGroupBookCount(group.groupId)
|
||||||
} else {
|
} else {
|
||||||
flowOf(systemCountsMap[group.groupId] ?: 0)
|
flowOf(systemCountsMap[group.groupId] ?: 0)
|
||||||
@@ -372,7 +436,7 @@ class BookshelfViewModel(
|
|||||||
GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
|
GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val dataStateFlow = combine(
|
private val dataCoreFlow = combine(
|
||||||
booksFlow,
|
booksFlow,
|
||||||
groupsFlow,
|
groupsFlow,
|
||||||
allGroupsFlow,
|
allGroupsFlow,
|
||||||
@@ -380,14 +444,21 @@ class BookshelfViewModel(
|
|||||||
internalStateFlow
|
internalStateFlow
|
||||||
) { books, groups, allGroups, previews, internal ->
|
) { books, groups, allGroups, previews, internal ->
|
||||||
BookshelfDataCore(books, groups, allGroups, previews, internal)
|
BookshelfDataCore(books, groups, allGroups, previews, internal)
|
||||||
}.combine(allGroupBooksImmutableFlow) { core, allGroupBooks ->
|
}
|
||||||
|
|
||||||
|
private val dataStateFlow = combine(
|
||||||
|
dataCoreFlow,
|
||||||
|
allGroupBooksImmutableFlow,
|
||||||
|
privacyModeFlow
|
||||||
|
) { core, allGroupBooks, privacyModeEnabled ->
|
||||||
BookshelfDataState(
|
BookshelfDataState(
|
||||||
books = core.books,
|
books = core.books,
|
||||||
groups = core.groups.map { it.toBookGroupUi() },
|
groups = core.groups.map { it.toBookGroupUi() },
|
||||||
allGroups = core.allGroups.map { it.toBookGroupUi() },
|
allGroups = core.allGroups.map { it.toBookGroupUi() },
|
||||||
previews = core.previews,
|
previews = core.previews,
|
||||||
internal = core.internal,
|
internal = core.internal,
|
||||||
allGroupBooks = allGroupBooks
|
allGroupBooks = allGroupBooks,
|
||||||
|
privacyModeEnabled = privacyModeEnabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +476,8 @@ class BookshelfViewModel(
|
|||||||
val allGroups: List<BookGroupUi>,
|
val allGroups: List<BookGroupUi>,
|
||||||
val previews: GroupPreviewState,
|
val previews: GroupPreviewState,
|
||||||
val internal: InternalState,
|
val internal: InternalState,
|
||||||
val allGroupBooks: ImmutableMap<Long, ImmutableList<BookUiItem>>
|
val allGroupBooks: ImmutableMap<Long, ImmutableList<BookUiItem>>,
|
||||||
|
val privacyModeEnabled: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
val uiState: StateFlow<BookshelfUiState> = combine(
|
val uiState: StateFlow<BookshelfUiState> = combine(
|
||||||
@@ -459,6 +531,7 @@ class BookshelfViewModel(
|
|||||||
bookGroupStyle = interaction.bookGroupStyle,
|
bookGroupStyle = interaction.bookGroupStyle,
|
||||||
bookshelfSort = internal.sortConfig.sort,
|
bookshelfSort = internal.sortConfig.sort,
|
||||||
bookshelfSortOrder = internal.sortConfig.sortOrder,
|
bookshelfSortOrder = internal.sortConfig.sortOrder,
|
||||||
|
privacyModeEnabled = data.privacyModeEnabled,
|
||||||
title = title,
|
title = title,
|
||||||
subtitle = when {
|
subtitle = when {
|
||||||
interaction.isEditMode -> {
|
interaction.isEditMode -> {
|
||||||
@@ -506,6 +579,14 @@ class BookshelfViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it }
|
groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it }
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
combine(privacyModeFlow, allGroupsFlow, groupIdFlow) {
|
||||||
|
privacyModeEnabled, groups, groupId ->
|
||||||
|
privacyModeEnabled && groups.any { it.groupId == groupId && it.isPrivate }
|
||||||
|
}.distinctUntilChanged().filter { it }.collect {
|
||||||
|
changeGroup(BookGroup.IdAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks ->
|
combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks ->
|
||||||
books to canReorderBooks
|
books to canReorderBooks
|
||||||
@@ -584,6 +665,12 @@ class BookshelfViewModel(
|
|||||||
clearSelection()
|
clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPrivacyMode(enabled: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.putBoolean(PreferKey.bookshelfPrivacyMode, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun showOverlay(overlay: BookshelfOverlay) {
|
fun showOverlay(overlay: BookshelfOverlay) {
|
||||||
activeOverlayFlow.value = overlay
|
activeOverlayFlow.value = overlay
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,9 +466,11 @@ private fun CoverTextOverlay(
|
|||||||
val startX = viewWidth * 0.84f
|
val startX = viewWidth * 0.84f
|
||||||
val fm = paint.fontMetrics
|
val fm = paint.fontMetrics
|
||||||
val charHeight = fm.bottom - fm.top
|
val charHeight = fm.bottom - fm.top
|
||||||
var startY = viewHeight * 0.16f - (author.length * charHeight)
|
val visibleAuthor = author.takeLast(
|
||||||
startY = startY.coerceAtLeast(viewHeight * 0.2f)
|
((viewHeight * 0.35f) / charHeight).toInt().coerceAtLeast(1)
|
||||||
author.forEach { char ->
|
)
|
||||||
|
var startY = viewHeight * 0.92f - (visibleAuthor.length - 1) * charHeight
|
||||||
|
visibleAuthor.forEach { char ->
|
||||||
nativeCanvas.drawText(char.toString(), startX, startY, paint)
|
nativeCanvas.drawText(char.toString(), startX, startY, paint)
|
||||||
startY += charHeight
|
startY += charHeight
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
|
import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
|
||||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.basic.Check
|
import top.yukonga.miuix.kmp.icon.basic.Check
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
@@ -50,6 +51,7 @@ fun RoundDropdownMenuItem(
|
|||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
hidden: Boolean = false,
|
||||||
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
|
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
) {
|
) {
|
||||||
@@ -88,7 +90,9 @@ fun RoundDropdownMenuItem(
|
|||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
MiuixText(
|
MiuixText(
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
modifier = Modifier
|
||||||
|
.widthIn(max = 200.dp)
|
||||||
|
.hiddenAttributeUnderline(hidden, textColor),
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -157,7 +161,9 @@ fun RoundDropdownMenuItem(
|
|||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
modifier = Modifier
|
||||||
|
.widthIn(max = 200.dp)
|
||||||
|
.hiddenAttributeUnderline(hidden, contentColor),
|
||||||
text = text,
|
text = text,
|
||||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||||
color = contentColor
|
color = contentColor
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
|
||||||
import top.yukonga.miuix.kmp.basic.TabRowDefaults
|
import top.yukonga.miuix.kmp.basic.TabRowDefaults
|
||||||
import top.yukonga.miuix.kmp.basic.TabRowWithContour
|
import top.yukonga.miuix.kmp.basic.TabRowWithContour
|
||||||
|
|
||||||
@@ -21,11 +22,12 @@ fun AppTabRow(
|
|||||||
selectedTabIndex: Int,
|
selectedTabIndex: Int,
|
||||||
onTabSelected: (Int) -> Unit,
|
onTabSelected: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isScrollable: Boolean = true
|
isScrollable: Boolean = true,
|
||||||
|
hiddenTabIndices: Set<Int> = emptySet()
|
||||||
) {
|
) {
|
||||||
val composeEngine = LegadoTheme.composeEngine
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
|
||||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
if (ThemeResolver.isMiuixEngine(composeEngine) && hiddenTabIndices.isEmpty()) {
|
||||||
TabRowWithContour(
|
TabRowWithContour(
|
||||||
tabs = tabTitles,
|
tabs = tabTitles,
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
@@ -50,7 +52,8 @@ fun AppTabRow(
|
|||||||
AppTab(
|
AppTab(
|
||||||
selected = selectedTabIndex == index,
|
selected = selectedTabIndex == index,
|
||||||
onClick = { onTabSelected(index) },
|
onClick = { onTabSelected(index) },
|
||||||
title = title
|
title = title,
|
||||||
|
hidden = index in hiddenTabIndices
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +68,8 @@ fun AppTabRow(
|
|||||||
AppTab(
|
AppTab(
|
||||||
selected = selectedTabIndex == index,
|
selected = selectedTabIndex == index,
|
||||||
onClick = { onTabSelected(index) },
|
onClick = { onTabSelected(index) },
|
||||||
title = title
|
title = title,
|
||||||
|
hidden = index in hiddenTabIndices
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,8 @@ fun AppTabRow(
|
|||||||
private fun AppTab(
|
private fun AppTab(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
title: String
|
title: String,
|
||||||
|
hidden: Boolean
|
||||||
) {
|
) {
|
||||||
Tab(
|
Tab(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
@@ -88,7 +93,13 @@ private fun AppTab(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.hiddenAttributeUnderline(
|
||||||
|
hidden,
|
||||||
|
if (selected) LegadoTheme.colorScheme.primary
|
||||||
|
else LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
),
|
||||||
color = if (selected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
color = if (selected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package io.legado.app.ui.widget.components.text
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun Modifier.hiddenAttributeUnderline(
|
||||||
|
hidden: Boolean,
|
||||||
|
color: Color
|
||||||
|
): Modifier = if (!hidden) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
drawBehind {
|
||||||
|
val strokeWidth = 1.dp.toPx()
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = androidx.compose.ui.geometry.Offset(0f, size.height - strokeWidth),
|
||||||
|
end = androidx.compose.ui.geometry.Offset(size.width, size.height - strokeWidth),
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 3.dp.toPx()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!--App-->
|
<!--App-->
|
||||||
<string name="app_name">阅读</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="app_name_a">阅读·A</string>
|
<string name="app_name_a">且看·A</string>
|
||||||
<string name="receiving_shared_label">阅读 · 搜索</string>
|
<string name="receiving_shared_label">阅读 · 搜索</string>
|
||||||
<string name="tip_perm_request_storage">阅读需要访问存储卡权限,请前往“设置”—“应用权限”,打开所需权限</string>
|
<string name="tip_perm_request_storage">阅读需要访问存储卡权限,请前往“设置”—“应用权限”,打开所需权限</string>
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<string name="change_origin">换源</string>
|
<string name="change_origin">换源</string>
|
||||||
<string name="about_description">全新设计的阅读 3.0</string>
|
<string name="about_description">全新设计的阅读 3.0</string>
|
||||||
<string name="app_share_description">
|
<string name="app_share_description">
|
||||||
阅读 3.0 下载地址:\nhttps://github.com/gedoor/legado/releases
|
且看 3.0 下载地址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
|
||||||
</string>
|
</string>
|
||||||
<string name="version_name">版本 %s</string>
|
<string name="version_name">版本 %s</string>
|
||||||
<string name="pt_background_verification">后台校验书源</string>
|
<string name="pt_background_verification">后台校验书源</string>
|
||||||
@@ -1295,9 +1295,10 @@
|
|||||||
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
||||||
<string name="create_folder">创建文件夹</string>
|
<string name="create_folder">创建文件夹</string>
|
||||||
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
||||||
<string name="private_group">设为隐私分组</string>
|
<string name="private_group">设为隐藏分组</string>
|
||||||
<string name="disable_private_group">关闭隐私分组</string>
|
<string name="disable_private_group">关闭隐藏分组</string>
|
||||||
<string name="private_group_desc">开启后,本分组内的书籍将从公开书架隐藏</string>
|
<string name="private_group_desc">开启后,本分组及其中书籍将在隐私模式下隐藏</string>
|
||||||
|
<string name="privacy_mode">隐私模式</string>
|
||||||
<string name="text_underline">文字下划线</string>
|
<string name="text_underline">文字下划线</string>
|
||||||
<string name="select_new_source">选中新增源</string>
|
<string name="select_new_source">选中新增源</string>
|
||||||
<string name="select_update_source">选中更新源</string>
|
<string name="select_update_source">选中更新源</string>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!--App-->
|
<!--App-->
|
||||||
<string name="app_name">閲讀</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="app_name_a">閲讀·A</string>
|
<string name="app_name_a">且看·A</string>
|
||||||
<string name="receiving_shared_label">閲讀·搜尋</string>
|
<string name="receiving_shared_label">閲讀·搜尋</string>
|
||||||
<string name="tip_perm_request_storage">閲讀需要訪問存儲卡權限,請前往「設定」—「應用程式權限」—開啟所需要的權限</string>
|
<string name="tip_perm_request_storage">閲讀需要訪問存儲卡權限,請前往「設定」—「應用程式權限」—開啟所需要的權限</string>
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
\u3000\u3000這是一款使用 Kotlin 全新開發的開源的閲讀應用程式,歡迎你的加入。
|
\u3000\u3000這是一款使用 Kotlin 全新開發的開源的閲讀應用程式,歡迎你的加入。
|
||||||
</string>
|
</string>
|
||||||
<string name="app_share_description">
|
<string name="app_share_description">
|
||||||
閲讀3.0下載地址:\nhttps://github.com/gedoor/legado/releases
|
且看 3.0 下載地址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
|
||||||
</string>
|
</string>
|
||||||
<string name="version_name">Version %s</string>
|
<string name="version_name">Version %s</string>
|
||||||
<string name="pt_background_verification">後臺校驗書源</string>
|
<string name="pt_background_verification">後臺校驗書源</string>
|
||||||
@@ -1145,9 +1145,10 @@
|
|||||||
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
||||||
<string name="create_folder">创建文件夹</string>
|
<string name="create_folder">创建文件夹</string>
|
||||||
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
||||||
<string name="private_group">設為私隱分組</string>
|
<string name="private_group">設為隱藏分組</string>
|
||||||
<string name="disable_private_group">關閉私隱分組</string>
|
<string name="disable_private_group">關閉隱藏分組</string>
|
||||||
<string name="private_group_desc">開啟後,本分組內嘅書籍將從公開書架隱藏</string>
|
<string name="private_group_desc">開啟後,本分組及其中書籍會在私隱模式下隱藏</string>
|
||||||
|
<string name="privacy_mode">私隱模式</string>
|
||||||
<string name="text_underline">文字下划线</string>
|
<string name="text_underline">文字下划线</string>
|
||||||
<string name="select_new_source">选中新增源</string>
|
<string name="select_new_source">选中新增源</string>
|
||||||
<string name="select_update_source">选中更新源</string>
|
<string name="select_update_source">选中更新源</string>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!--App-->
|
<!--App-->
|
||||||
<string name="app_name">閱讀</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="app_name_a">閱讀·A</string>
|
<string name="app_name_a">且看·A</string>
|
||||||
<string name="receiving_shared_label">閱讀·搜尋</string>
|
<string name="receiving_shared_label">閱讀·搜尋</string>
|
||||||
<string name="tip_perm_request_storage">閱讀需要存取記憶卡權限,請前往「設定」—「應用程式權限」—打開所需權限</string>
|
<string name="tip_perm_request_storage">閱讀需要存取記憶卡權限,請前往「設定」—「應用程式權限」—打開所需權限</string>
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
\u3000\u3000這是一款使用Kotlin全新開發的開源的閱讀軟體,歡迎您的加入。
|
\u3000\u3000這是一款使用Kotlin全新開發的開源的閱讀軟體,歡迎您的加入。
|
||||||
</string>
|
</string>
|
||||||
<string name="app_share_description">
|
<string name="app_share_description">
|
||||||
閱讀3.0下載網址:\nhttps://github.com/gedoor/legado/releases
|
且看 3.0 下載網址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
|
||||||
</string>
|
</string>
|
||||||
<string name="version_name">Version %s</string>
|
<string name="version_name">Version %s</string>
|
||||||
<string name="pt_background_verification">後臺校驗書源</string>
|
<string name="pt_background_verification">後臺校驗書源</string>
|
||||||
@@ -1148,9 +1148,10 @@
|
|||||||
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
<string name="file_manage_summary">管理私有文件夹的文件</string>
|
||||||
<string name="create_folder">创建文件夹</string>
|
<string name="create_folder">创建文件夹</string>
|
||||||
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
<string name="allow_drop_down_refresh">允许下拉刷新</string>
|
||||||
<string name="private_group">設為隱私分組</string>
|
<string name="private_group">設為隱藏分組</string>
|
||||||
<string name="disable_private_group">關閉隱私分組</string>
|
<string name="disable_private_group">關閉隱藏分組</string>
|
||||||
<string name="private_group_desc">開啟後,本分組內的書籍將從公開書架隱藏</string>
|
<string name="private_group_desc">開啟後,本分組及其中書籍將在隱私模式下隱藏</string>
|
||||||
|
<string name="privacy_mode">隱私模式</string>
|
||||||
<string name="text_underline">文字下划线</string>
|
<string name="text_underline">文字下划线</string>
|
||||||
<string name="select_new_source">选中新增源</string>
|
<string name="select_new_source">选中新增源</string>
|
||||||
<string name="select_update_source">选中更新源</string>
|
<string name="select_update_source">选中更新源</string>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<string name="email" translatable="false">gekunfei@live.com</string>
|
<string name="email" translatable="false">gekunfei@live.com</string>
|
||||||
|
|
||||||
<string name="contributors_url" translatable="false">https://github.com/gedoor/legado/graphs/contributors</string>
|
<string name="contributors_url" translatable="false">https://github.com/gedoor/legado/graphs/contributors</string>
|
||||||
<string name="github_url" translatable="false">https://github.com/HapeLee/legado-with-MD3</string>
|
<string name="github_url" translatable="false">https://gitea.yamby.cn/yusheng/QieKan-3.0</string>
|
||||||
<string name="legado_url" translatable="false">https://gedoor.github.io/</string>
|
<string name="legado_url" translatable="false">https://gedoor.github.io/</string>
|
||||||
<string name="http_ip" translatable="false">http://%1$s:%2$d</string>
|
<string name="http_ip" translatable="false">http://%1$s:%2$d</string>
|
||||||
<string name="git_hub" translatable="false">GitHub</string>
|
<string name="git_hub" translatable="false">Gitea</string>
|
||||||
<string name="diy_edit_source_group_title" translatable="false">【%s】</string>
|
<string name="diy_edit_source_group_title" translatable="false">【%s】</string>
|
||||||
<string name="vip_title" translatable="false">🔒%s</string>
|
<string name="vip_title" translatable="false">🔒%s</string>
|
||||||
<string name="payed_title" translatable="false">🔓%s</string>
|
<string name="payed_title" translatable="false">🔓%s</string>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!--App-->
|
<!--App-->
|
||||||
<string name="app_name">Legado</string>
|
<string name="app_name">且看</string>
|
||||||
<string name="app_name_a">Legado·A</string>
|
<string name="app_name_a">且看·A</string>
|
||||||
<string name="receiving_shared_label">Legado·search</string>
|
<string name="receiving_shared_label">Legado·search</string>
|
||||||
<string name="tip_perm_request_storage">Legado needs storage access to find and read books. Please go "App Settings" to allow "Storage Permission".</string>
|
<string name="tip_perm_request_storage">Legado needs storage access to find and read books. Please go "App Settings" to allow "Storage Permission".</string>
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
\u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us.
|
\u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us.
|
||||||
</string>
|
</string>
|
||||||
<string name="app_share_description">
|
<string name="app_share_description">
|
||||||
Legado (YueDu 3.0) download link:\n https://github.com/gedoor/legado/releases
|
QieKan 3.0 download link:\n https://gitea.yamby.cn/yusheng/QieKan-3.0/releases
|
||||||
</string>
|
</string>
|
||||||
<string name="version_name">Version %s</string>
|
<string name="version_name">Version %s</string>
|
||||||
<string name="pt_background_verification">Background-verification</string>
|
<string name="pt_background_verification">Background-verification</string>
|
||||||
@@ -1336,9 +1336,10 @@
|
|||||||
<string name="file_manage_summary">Managing files in the application\'s private folder</string>
|
<string name="file_manage_summary">Managing files in the application\'s private folder</string>
|
||||||
<string name="create_folder">Create Folder</string>
|
<string name="create_folder">Create Folder</string>
|
||||||
<string name="allow_drop_down_refresh">Allow drop-down refresh</string>
|
<string name="allow_drop_down_refresh">Allow drop-down refresh</string>
|
||||||
<string name="private_group">Set as private group</string>
|
<string name="private_group">Set as hidden group</string>
|
||||||
<string name="disable_private_group">Disable private group</string>
|
<string name="disable_private_group">Disable hidden group</string>
|
||||||
<string name="private_group_desc">After enabling, books in this group are hidden from the public bookshelf</string>
|
<string name="private_group_desc">The group and its books are hidden while privacy mode is enabled</string>
|
||||||
|
<string name="privacy_mode">Privacy mode</string>
|
||||||
<string name="text_underline">Text underline</string>
|
<string name="text_underline">Text underline</string>
|
||||||
<string name="select_new_source">Select new source</string>
|
<string name="select_new_source">Select new source</string>
|
||||||
<string name="select_update_source">Select update source</string>
|
<string name="select_update_source">Select update source</string>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class BookPrivacyStateTest {
|
||||||
|
|
||||||
|
private val hiddenGroup = BookGroup(groupId = 32L, isPrivate = true)
|
||||||
|
private val visibleBook = Book(
|
||||||
|
bookUrl = "visible-url",
|
||||||
|
name = "MS攻略",
|
||||||
|
author = "公开作者",
|
||||||
|
group = 0L,
|
||||||
|
)
|
||||||
|
private val hiddenBook = Book(
|
||||||
|
bookUrl = "hidden-url",
|
||||||
|
name = "秘密书",
|
||||||
|
author = "隐藏作者",
|
||||||
|
group = 32L,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `privacy mode only exposes records linked to visible books`() {
|
||||||
|
val state = BookPrivacyState.create(
|
||||||
|
books = listOf(visibleBook, hiddenBook),
|
||||||
|
groups = listOf(hiddenGroup),
|
||||||
|
privacyModeEnabled = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(state.isReadRecordVisible("visible-url", "旧标题"))
|
||||||
|
assertTrue(state.isReadRecordVisible(null, "MS 攻略", "公开作者"))
|
||||||
|
assertTrue(state.isReadRecordVisible("stale-url", "MS 攻略", "公开作者"))
|
||||||
|
assertFalse(state.isReadRecordVisible("hidden-url", "任意标题"))
|
||||||
|
assertFalse(state.isReadRecordVisible(null, "秘密书"))
|
||||||
|
assertFalse(state.isReadRecordVisible("stale-url", "秘密书"))
|
||||||
|
assertFalse(state.isReadRecordVisible(null, "无法关联的历史别名"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `author disambiguates visible and hidden books with the same title`() {
|
||||||
|
val state = BookPrivacyState.create(
|
||||||
|
books = listOf(
|
||||||
|
visibleBook.copy(name = "同名书"),
|
||||||
|
hiddenBook.copy(name = "同名书"),
|
||||||
|
),
|
||||||
|
groups = listOf(hiddenGroup),
|
||||||
|
privacyModeEnabled = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(state.isReadRecordVisible("stale-url", "同名书", "公开作者"))
|
||||||
|
assertFalse(state.isReadRecordVisible("stale-url", "同名书", "隐藏作者"))
|
||||||
|
assertFalse(state.isReadRecordVisible("stale-url", "同名书"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `privacy mode off exposes all historical records`() {
|
||||||
|
val state = BookPrivacyState.create(
|
||||||
|
books = listOf(visibleBook, hiddenBook),
|
||||||
|
groups = listOf(hiddenGroup),
|
||||||
|
privacyModeEnabled = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(state.isReadRecordVisible("hidden-url", "秘密书"))
|
||||||
|
assertTrue(state.isReadRecordVisible(null, "无法关联的历史别名"))
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.mutablePreferencesOf
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SettingsRepositoryCompatibilityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun normalizeLegacyBrightnessVisibilityBoolean() {
|
||||||
|
assertEquals("1", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, true))
|
||||||
|
assertEquals("0", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, false))
|
||||||
|
assertEquals("2", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, "2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun readLegacyBrightnessVisibilityBooleanAsString() {
|
||||||
|
val enabled = mutablePreferencesOf(
|
||||||
|
booleanPreferencesKey(PreferKey.showBrightnessView) to true
|
||||||
|
)
|
||||||
|
val disabled = mutablePreferencesOf(
|
||||||
|
booleanPreferencesKey(PreferKey.showBrightnessView) to false
|
||||||
|
)
|
||||||
|
val vertical = mutablePreferencesOf(
|
||||||
|
stringPreferencesKey(PreferKey.showBrightnessView) to "2"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("1", enabled.getStringCompat(PreferKey.showBrightnessView, "0"))
|
||||||
|
assertEquals("0", disabled.getStringCompat(PreferKey.showBrightnessView, "1"))
|
||||||
|
assertEquals("2", vertical.getStringCompat(PreferKey.showBrightnessView, "1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class BookShelfItemTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `marks book in highest-bit private group as hidden`() {
|
||||||
|
val book = BookShelfItem(
|
||||||
|
bookUrl = "book-url",
|
||||||
|
name = "book",
|
||||||
|
author = "author",
|
||||||
|
origin = "origin",
|
||||||
|
originName = "origin-name",
|
||||||
|
coverUrl = null,
|
||||||
|
customCoverUrl = null,
|
||||||
|
durChapterTitle = null,
|
||||||
|
durChapterTime = 0L,
|
||||||
|
durChapterPos = 0,
|
||||||
|
latestChapterTitle = null,
|
||||||
|
latestChapterTime = 0L,
|
||||||
|
lastCheckCount = 0,
|
||||||
|
totalChapterNum = 0,
|
||||||
|
durChapterIndex = 0,
|
||||||
|
type = 0,
|
||||||
|
group = Long.MIN_VALUE,
|
||||||
|
order = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(book.toUiItem(privateGroupMask = Long.MIN_VALUE).isHidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
VERSION_MAJOR=3
|
VERSION_MAJOR=3
|
||||||
VERSION_MINOR=26
|
VERSION_MINOR=26
|
||||||
VERSION_PATCH=13
|
VERSION_PATCH=14
|
||||||
VERSION_SUFFIX=1
|
VERSION_SUFFIX=1
|
||||||
# 1 = Pre, 0 = Release
|
# 1 = Pre, 0 = Release
|
||||||
Reference in New Issue
Block a user