油画首页
This commit is contained in:
39 files changed
+2123
-1509
No files matched your search
@@ -44,9 +44,11 @@ object PreferKey {
|
|||||||
const val ttsSpeechRate = "ttsSpeechRate"
|
const val ttsSpeechRate = "ttsSpeechRate"
|
||||||
const val prevKeys = "prevKeyCodes"
|
const val prevKeys = "prevKeyCodes"
|
||||||
const val nextKeys = "nextKeyCodes"
|
const val nextKeys = "nextKeyCodes"
|
||||||
|
const val showHome = "showHome"
|
||||||
const val showDiscovery = "showDiscovery"
|
const val showDiscovery = "showDiscovery"
|
||||||
const val enableReview = "enableReview"
|
const val enableReview = "enableReview"
|
||||||
const val showRss = "showRss"
|
const val showRss = "showRss"
|
||||||
|
const val mainNavigationOrder = "mainNavigationOrder"
|
||||||
const val showStatusBar = "showStatusBar"
|
const val showStatusBar = "showStatusBar"
|
||||||
const val swipeAnimation = "swipeAnimation"
|
const val swipeAnimation = "swipeAnimation"
|
||||||
const val bookshelfLayout = "bookshelfLayout"
|
const val bookshelfLayout = "bookshelfLayout"
|
||||||
@@ -364,9 +366,6 @@ object PreferKey {
|
|||||||
const val llmTemperature = "llmTemperature"
|
const val llmTemperature = "llmTemperature"
|
||||||
const val llmPrompt = "llmPrompt"
|
const val llmPrompt = "llmPrompt"
|
||||||
|
|
||||||
const val homepageModuleOrder = "homepageModuleOrder"
|
|
||||||
const val homepageModuleHidden = "homepageModuleHidden"
|
|
||||||
const val homepageLayoutMode = "homepageLayoutMode"
|
|
||||||
const val navIconHome = "navIconHome"
|
const val navIconHome = "navIconHome"
|
||||||
|
|
||||||
// Lab
|
// Lab
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
|
import io.legado.app.data.entities.readRecord.HomeRecentBookRow
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||||
@@ -25,6 +26,56 @@ interface ReadRecordDao {
|
|||||||
@Query("SELECT sum(readTime) FROM readRecord")
|
@Query("SELECT sum(readTime) FROM readRecord")
|
||||||
fun getTotalReadTime(): Flow<Long?>
|
fun getTotalReadTime(): Flow<Long?>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM (
|
||||||
|
SELECT 1
|
||||||
|
FROM readRecord
|
||||||
|
GROUP BY bookName, bookAuthor
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeTotalReadBookCount(): Flow<Int>
|
||||||
|
|
||||||
|
@Query("SELECT SUM(readTime) FROM readRecordDetail WHERE date = :date")
|
||||||
|
fun observeReadTimeByDate(date: String): Flow<Long?>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
WITH recent AS (
|
||||||
|
SELECT
|
||||||
|
bookName,
|
||||||
|
bookAuthor,
|
||||||
|
MAX(lastRead) AS lastRead
|
||||||
|
FROM readRecord
|
||||||
|
GROUP BY bookName, bookAuthor
|
||||||
|
ORDER BY lastRead DESC
|
||||||
|
LIMIT :limit
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
recent.bookName AS recordName,
|
||||||
|
recent.bookAuthor AS recordAuthor,
|
||||||
|
book.bookUrl AS bookUrl,
|
||||||
|
book.origin AS origin,
|
||||||
|
book.coverUrl AS coverUrl,
|
||||||
|
book.customCoverUrl AS customCoverUrl,
|
||||||
|
book.durChapterTitle AS chapterTitle,
|
||||||
|
book.totalChapterNum AS totalChapterNum,
|
||||||
|
book.durChapterIndex AS chapterIndex
|
||||||
|
FROM recent
|
||||||
|
LEFT JOIN books AS book ON book.bookUrl = (
|
||||||
|
SELECT candidate.bookUrl
|
||||||
|
FROM books AS candidate
|
||||||
|
WHERE candidate.name = recent.bookName
|
||||||
|
AND candidate.author = recent.bookAuthor
|
||||||
|
ORDER BY candidate.durChapterTime DESC, candidate.bookUrl ASC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
ORDER BY recent.lastRead DESC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun observeRecentHomeBooks(limit: Int): 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?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package io.legado.app.data.entities.readRecord
|
||||||
|
|
||||||
|
data class HomeRecentBookRow(
|
||||||
|
val recordName: String,
|
||||||
|
val recordAuthor: String,
|
||||||
|
val bookUrl: String?,
|
||||||
|
val origin: String?,
|
||||||
|
val coverUrl: String?,
|
||||||
|
val customCoverUrl: String?,
|
||||||
|
val chapterTitle: String?,
|
||||||
|
val totalChapterNum: Int?,
|
||||||
|
val chapterIndex: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
@@ -19,13 +19,14 @@ object LocalPreferencesKeys {
|
|||||||
val EXPLORE_LAYOUT_MODE = intPreferencesKey("explore_layout_mode")
|
val EXPLORE_LAYOUT_MODE = intPreferencesKey("explore_layout_mode")
|
||||||
val EXPLORE_LAYOUT_GRID_PORTRAIT = intPreferencesKey("explore_layout_grid_portrait")
|
val EXPLORE_LAYOUT_GRID_PORTRAIT = intPreferencesKey("explore_layout_grid_portrait")
|
||||||
val EXPLORE_LAYOUT_GRID_LANDSCAPE = intPreferencesKey("explore_layout_grid_landscape")
|
val EXPLORE_LAYOUT_GRID_LANDSCAPE = intPreferencesKey("explore_layout_grid_landscape")
|
||||||
val EXPLORE_STYLE = stringPreferencesKey("explore_style")
|
|
||||||
val READ_URL_IN_BROWSER = booleanPreferencesKey("read_url_in_browser")
|
val READ_URL_IN_BROWSER = booleanPreferencesKey("read_url_in_browser")
|
||||||
val LAST_BACKUP = longPreferencesKey("last_backup")
|
val LAST_BACKUP = longPreferencesKey("last_backup")
|
||||||
val PASSWORD = stringPreferencesKey("password")
|
val PASSWORD = stringPreferencesKey("password")
|
||||||
val PRIVACY_POLICY_OK = booleanPreferencesKey("privacy_policy_ok")
|
val PRIVACY_POLICY_OK = booleanPreferencesKey("privacy_policy_ok")
|
||||||
val PERMISSION_CHECKED = booleanPreferencesKey("permission_checked")
|
val PERMISSION_CHECKED = booleanPreferencesKey("permission_checked")
|
||||||
val DAILY_READING_GOAL_MINUTES = intPreferencesKey("daily_reading_goal_minutes")
|
val DAILY_READING_GOAL_MINUTES = intPreferencesKey("daily_reading_goal_minutes")
|
||||||
|
val HOME_SOURCE_SET_URL = stringPreferencesKey("home_source_set_url")
|
||||||
|
val HOME_DASHBOARD_SECTIONS = stringPreferencesKey("home_dashboard_sections")
|
||||||
|
|
||||||
// Change source options
|
// Change source options
|
||||||
val CHANGE_SOURCE_CHECK_AUTHOR = booleanPreferencesKey("changeSourceCheckAuthor")
|
val CHANGE_SOURCE_CHECK_AUTHOR = booleanPreferencesKey("changeSourceCheckAuthor")
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.data.dao.ReadRecordDao
|
||||||
|
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
||||||
|
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||||
|
import io.legado.app.domain.gateway.HomeDashboardGateway
|
||||||
|
import io.legado.app.domain.model.DEFAULT_HOME_DASHBOARD_SECTIONS
|
||||||
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
|
import io.legado.app.domain.model.HomeReadingBook
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class HomeDashboardRepository(
|
||||||
|
private val readRecordDao: ReadRecordDao,
|
||||||
|
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||||
|
) : HomeDashboardGateway {
|
||||||
|
|
||||||
|
override fun observeTotalReadBooks(): Flow<Int> =
|
||||||
|
readRecordDao.observeTotalReadBookCount()
|
||||||
|
|
||||||
|
override fun observeTotalReadTime(): Flow<Long> =
|
||||||
|
readRecordDao.getTotalReadTime().map { it ?: 0L }
|
||||||
|
|
||||||
|
override fun observeReadTime(date: String): Flow<Long> =
|
||||||
|
readRecordDao.observeReadTimeByDate(date).map { it ?: 0L }
|
||||||
|
|
||||||
|
override fun observeRecentBooks(limit: Int): Flow<List<HomeReadingBook>> =
|
||||||
|
readRecordDao.observeRecentHomeBooks(limit).map { rows ->
|
||||||
|
rows.map { row ->
|
||||||
|
HomeReadingBook(
|
||||||
|
bookUrl = row.bookUrl,
|
||||||
|
name = row.recordName,
|
||||||
|
author = row.recordAuthor,
|
||||||
|
origin = row.origin,
|
||||||
|
coverPath = if (row.customCoverUrl.isNullOrEmpty()) {
|
||||||
|
row.coverUrl
|
||||||
|
} else {
|
||||||
|
row.customCoverUrl
|
||||||
|
},
|
||||||
|
chapterTitle = row.chapterTitle,
|
||||||
|
chapterProgress = if (
|
||||||
|
row.totalChapterNum != null &&
|
||||||
|
row.totalChapterNum > 0 &&
|
||||||
|
row.chapterIndex != null
|
||||||
|
) {
|
||||||
|
(row.chapterIndex + 1)
|
||||||
|
.coerceIn(0, row.totalChapterNum)
|
||||||
|
.toFloat() / row.totalChapterNum
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeDailyGoal(defaultValue: Int): Flow<Int> =
|
||||||
|
localPreferencesRepository.getPreference(
|
||||||
|
LocalPreferencesKeys.DAILY_READING_GOAL_MINUTES,
|
||||||
|
defaultValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun observeSelectedSourceSetUrl(): Flow<String?> =
|
||||||
|
localPreferencesRepository.getPreference(
|
||||||
|
LocalPreferencesKeys.HOME_SOURCE_SET_URL,
|
||||||
|
"",
|
||||||
|
).map { it.takeIf(String::isNotBlank) }
|
||||||
|
|
||||||
|
override fun observeVisibleSections(): Flow<Set<HomeDashboardSection>> =
|
||||||
|
localPreferencesRepository.getPreference(
|
||||||
|
LocalPreferencesKeys.HOME_DASHBOARD_SECTIONS,
|
||||||
|
DEFAULT_HOME_DASHBOARD_SECTIONS.joinToString(",") { it.storageValue },
|
||||||
|
).map(HomeDashboardSection::fromStorage)
|
||||||
|
|
||||||
|
override suspend fun updateDailyGoal(minutes: Int) {
|
||||||
|
localPreferencesRepository.updatePreference(
|
||||||
|
LocalPreferencesKeys.DAILY_READING_GOAL_MINUTES,
|
||||||
|
minutes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateSelectedSourceSetUrl(sourceUrl: String) {
|
||||||
|
localPreferencesRepository.updatePreference(
|
||||||
|
LocalPreferencesKeys.HOME_SOURCE_SET_URL,
|
||||||
|
sourceUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateVisibleSections(sections: Set<HomeDashboardSection>) {
|
||||||
|
localPreferencesRepository.updatePreference(
|
||||||
|
LocalPreferencesKeys.HOME_DASHBOARD_SECTIONS,
|
||||||
|
HomeDashboardSection.entries
|
||||||
|
.filter(sections::contains)
|
||||||
|
.joinToString(",") { it.storageValue },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package io.legado.app.data.repository
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
import androidx.room.Transaction
|
import androidx.room.withTransaction
|
||||||
import cn.hutool.core.date.DatePattern
|
import cn.hutool.core.date.DatePattern
|
||||||
import cn.hutool.core.date.DateUtil
|
import cn.hutool.core.date.DateUtil
|
||||||
|
import io.legado.app.data.AppDatabase
|
||||||
import io.legado.app.data.dao.ReadRecordDao
|
import io.legado.app.data.dao.ReadRecordDao
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
@@ -15,7 +16,8 @@ import kotlin.math.max
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class ReadRecordRepository(
|
class ReadRecordRepository(
|
||||||
private val dao: ReadRecordDao
|
private val dao: ReadRecordDao,
|
||||||
|
private val database: AppDatabase,
|
||||||
) {
|
) {
|
||||||
private fun getCurrentDeviceId(): String = ""
|
private fun getCurrentDeviceId(): String = ""
|
||||||
|
|
||||||
@@ -85,11 +87,15 @@ class ReadRecordRepository(
|
|||||||
* 保存一个完整的阅读会话.
|
* 保存一个完整的阅读会话.
|
||||||
*/
|
*/
|
||||||
suspend fun saveReadSession(newSession: ReadRecordSession) {
|
suspend fun saveReadSession(newSession: ReadRecordSession) {
|
||||||
val segmentDuration = newSession.endTime - newSession.startTime
|
if (newSession.endTime <= newSession.startTime) return
|
||||||
dao.insertSession(newSession)
|
database.withTransaction {
|
||||||
val dateString = DateUtil.format(Date(newSession.startTime), DatePattern.NORM_DATE_PATTERN)
|
val segmentDuration = newSession.endTime - newSession.startTime
|
||||||
updateReadRecordDetail(newSession, segmentDuration, newSession.words, dateString)
|
dao.insertSession(newSession)
|
||||||
updateReadRecord(newSession, segmentDuration)
|
val dateString =
|
||||||
|
DateUtil.format(Date(newSession.startTime), DatePattern.NORM_DATE_PATTERN)
|
||||||
|
updateReadRecordDetail(newSession, segmentDuration, newSession.words, dateString)
|
||||||
|
updateReadRecord(newSession, segmentDuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateReadRecord(session: ReadRecordSession, durationDelta: Long) {
|
private suspend fun updateReadRecord(session: ReadRecordSession, durationDelta: Long) {
|
||||||
@@ -151,58 +157,72 @@ class ReadRecordRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteDetail(detail: ReadRecordDetail) {
|
suspend fun deleteDetail(detail: ReadRecordDetail) {
|
||||||
dao.deleteDetail(detail)
|
database.withTransaction {
|
||||||
dao.deleteSessionsByBookAndDate(
|
dao.deleteDetail(detail)
|
||||||
detail.deviceId,
|
dao.deleteSessionsByBookAndDate(
|
||||||
detail.bookName,
|
detail.deviceId,
|
||||||
detail.bookAuthor,
|
detail.bookName,
|
||||||
detail.date
|
detail.bookAuthor,
|
||||||
)
|
detail.date
|
||||||
updateReadRecordTotal(detail.deviceId, detail.bookName, detail.bookAuthor)
|
)
|
||||||
|
updateReadRecordTotal(detail.deviceId, detail.bookName, detail.bookAuthor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
|
||||||
suspend fun deleteSession(session: ReadRecordSession) {
|
suspend fun deleteSession(session: ReadRecordSession) {
|
||||||
dao.deleteSession(session)
|
database.withTransaction {
|
||||||
|
dao.deleteSession(session)
|
||||||
|
|
||||||
val dateString = DateUtil.format(Date(session.startTime), "yyyy-MM-dd")
|
val dateString = DateUtil.format(Date(session.startTime), "yyyy-MM-dd")
|
||||||
val remainingSessions =
|
val remainingSessions =
|
||||||
dao.getSessionsByBookAndDate(
|
dao.getSessionsByBookAndDate(
|
||||||
session.deviceId,
|
session.deviceId,
|
||||||
session.bookName,
|
session.bookName,
|
||||||
session.bookAuthor,
|
session.bookAuthor,
|
||||||
dateString
|
dateString
|
||||||
)
|
)
|
||||||
|
|
||||||
if (remainingSessions.isEmpty()) {
|
if (remainingSessions.isEmpty()) {
|
||||||
val detail = dao.getDetail(
|
val detail = dao.getDetail(
|
||||||
session.deviceId,
|
session.deviceId,
|
||||||
session.bookName,
|
session.bookName,
|
||||||
session.bookAuthor,
|
session.bookAuthor,
|
||||||
dateString
|
dateString
|
||||||
)
|
)
|
||||||
detail?.let { dao.deleteDetail(it) }
|
detail?.let { dao.deleteDetail(it) }
|
||||||
} else {
|
} else {
|
||||||
val totalTime = remainingSessions.sumOf { it.endTime - it.startTime }
|
val totalTime = remainingSessions.sumOf { it.endTime - it.startTime }
|
||||||
val totalWords = remainingSessions.sumOf { it.words }
|
val totalWords = remainingSessions.sumOf { it.words }
|
||||||
val firstRead = remainingSessions.minOf { it.startTime }
|
val firstRead = remainingSessions.minOf { it.startTime }
|
||||||
val lastRead = remainingSessions.maxOf { it.endTime }
|
val lastRead = remainingSessions.maxOf { it.endTime }
|
||||||
|
|
||||||
val existingDetail = dao.getDetail(
|
val existingDetail = dao.getDetail(
|
||||||
session.deviceId,
|
session.deviceId,
|
||||||
session.bookName,
|
session.bookName,
|
||||||
session.bookAuthor,
|
session.bookAuthor,
|
||||||
dateString
|
dateString
|
||||||
)
|
)
|
||||||
existingDetail?.copy(
|
dao.insertDetail(
|
||||||
readTime = totalTime,
|
existingDetail?.copy(
|
||||||
readWords = totalWords,
|
readTime = totalTime,
|
||||||
firstReadTime = firstRead,
|
readWords = totalWords,
|
||||||
lastReadTime = lastRead
|
firstReadTime = firstRead,
|
||||||
)?.let { dao.insertDetail(it) }
|
lastReadTime = lastRead
|
||||||
|
) ?: ReadRecordDetail(
|
||||||
|
deviceId = session.deviceId,
|
||||||
|
bookName = session.bookName,
|
||||||
|
bookAuthor = session.bookAuthor,
|
||||||
|
date = dateString,
|
||||||
|
readTime = totalTime,
|
||||||
|
readWords = totalWords,
|
||||||
|
firstReadTime = firstRead,
|
||||||
|
lastReadTime = lastRead,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReadRecordTotal(session.deviceId, session.bookName, session.bookAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateReadRecordTotal(session.deviceId, session.bookName, session.bookAuthor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateReadRecordTotal(deviceId: String, bookName: String, bookAuthor: String) {
|
private suspend fun updateReadRecordTotal(deviceId: String, bookName: String, bookAuthor: String) {
|
||||||
@@ -214,27 +234,44 @@ class ReadRecordRepository(
|
|||||||
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 }
|
||||||
|
|
||||||
dao.getReadRecord(deviceId, bookName, bookAuthor)?.copy(
|
val existingRecord = dao.getReadRecord(deviceId, bookName, bookAuthor)
|
||||||
readTime = totalTime,
|
if (existingRecord == null) {
|
||||||
lastRead = lastRead
|
dao.insert(
|
||||||
)?.let { dao.update(it) }
|
ReadRecord(
|
||||||
|
deviceId = deviceId,
|
||||||
|
bookName = bookName,
|
||||||
|
bookAuthor = bookAuthor,
|
||||||
|
readTime = totalTime,
|
||||||
|
lastRead = lastRead,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
dao.update(
|
||||||
|
existingRecord.copy(
|
||||||
|
readTime = totalTime,
|
||||||
|
lastRead = lastRead
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteReadRecord(record: ReadRecord) {
|
suspend fun deleteReadRecord(record: ReadRecord) {
|
||||||
dao.deleteReadRecord(record)
|
database.withTransaction {
|
||||||
dao.deleteDetailsByBook(record.deviceId, record.bookName, record.bookAuthor)
|
dao.deleteReadRecord(record)
|
||||||
dao.deleteSessionsByBook(record.deviceId, record.bookName, record.bookAuthor)
|
dao.deleteDetailsByBook(record.deviceId, record.bookName, record.bookAuthor)
|
||||||
|
dao.deleteSessionsByBook(record.deviceId, record.bookName, record.bookAuthor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
|
||||||
suspend fun mergeReadRecordInto(targetRecord: ReadRecord, sourceRecords: List<ReadRecord>) {
|
suspend fun mergeReadRecordInto(targetRecord: ReadRecord, sourceRecords: List<ReadRecord>) {
|
||||||
sourceRecords.forEach { sourceRecord ->
|
database.withTransaction {
|
||||||
mergeSingleReadRecordInto(targetRecord, sourceRecord)
|
sourceRecords.forEach { sourceRecord ->
|
||||||
|
mergeSingleReadRecordInto(targetRecord, sourceRecord)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
|
||||||
private suspend fun mergeSingleReadRecordInto(targetRecord: ReadRecord, sourceRecord: ReadRecord) {
|
private suspend fun mergeSingleReadRecordInto(targetRecord: ReadRecord, sourceRecord: ReadRecord) {
|
||||||
if (targetRecord == sourceRecord) return
|
if (targetRecord == sourceRecord) return
|
||||||
if (targetRecord.deviceId != sourceRecord.deviceId) return
|
if (targetRecord.deviceId != sourceRecord.deviceId) return
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class WebDavBackupRepository : WebDavBackupGateway {
|
|||||||
|
|
||||||
override suspend fun getLatestBackup(): WebDavBackup? {
|
override suspend fun getLatestBackup(): WebDavBackup? {
|
||||||
return withContext(IO) {
|
return withContext(IO) {
|
||||||
AppWebDav.lastBackUp().getOrNull()?.let {
|
AppWebDav.lastBackUp().getOrThrow()?.let {
|
||||||
WebDavBackup(
|
WebDavBackup(
|
||||||
name = it.displayName,
|
name = it.displayName,
|
||||||
lastModify = it.lastModify
|
lastModify = it.lastModify
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import coil.decode.ImageDecoderDecoder
|
|||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import io.legado.app.data.AppDatabase
|
import io.legado.app.data.AppDatabase
|
||||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||||
import io.legado.app.data.repository.AppStartupRepository
|
|
||||||
import io.legado.app.data.repository.BackupRestoreRepository
|
|
||||||
import io.legado.app.data.repository.AiArtifactRepository
|
import io.legado.app.data.repository.AiArtifactRepository
|
||||||
import io.legado.app.data.repository.AiChatRepository
|
import io.legado.app.data.repository.AiChatRepository
|
||||||
import io.legado.app.data.repository.AiMemoryRepository
|
import io.legado.app.data.repository.AiMemoryRepository
|
||||||
import io.legado.app.data.repository.AiProfileRepository
|
import io.legado.app.data.repository.AiProfileRepository
|
||||||
import io.legado.app.data.repository.AiTextRepositoryImpl
|
import io.legado.app.data.repository.AiTextRepositoryImpl
|
||||||
import io.legado.app.data.repository.AiToolRepository
|
import io.legado.app.data.repository.AiToolRepository
|
||||||
|
import io.legado.app.data.repository.AppStartupRepository
|
||||||
|
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
|
||||||
@@ -30,6 +30,7 @@ import io.legado.app.data.repository.DirectLinkUploadRepository
|
|||||||
import io.legado.app.data.repository.ExploreRepository
|
import io.legado.app.data.repository.ExploreRepository
|
||||||
import io.legado.app.data.repository.ExploreRepositoryImpl
|
import io.legado.app.data.repository.ExploreRepositoryImpl
|
||||||
import io.legado.app.data.repository.HighlightRuleRepository
|
import io.legado.app.data.repository.HighlightRuleRepository
|
||||||
|
import io.legado.app.data.repository.HomeDashboardRepository
|
||||||
import io.legado.app.data.repository.HomepageModulesRepository
|
import io.legado.app.data.repository.HomepageModulesRepository
|
||||||
import io.legado.app.data.repository.LocalBookRepository
|
import io.legado.app.data.repository.LocalBookRepository
|
||||||
import io.legado.app.data.repository.ReadAloudSettingsRepository
|
import io.legado.app.data.repository.ReadAloudSettingsRepository
|
||||||
@@ -47,14 +48,14 @@ import io.legado.app.data.repository.TranslationCacheRepositoryImpl
|
|||||||
import io.legado.app.data.repository.UploadRepository
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.data.repository.WebDavBackupRepository
|
import io.legado.app.data.repository.WebDavBackupRepository
|
||||||
import io.legado.app.data.repository.WebDavReadingProgressRepository
|
import io.legado.app.data.repository.WebDavReadingProgressRepository
|
||||||
import io.legado.app.domain.gateway.AppStartupGateway
|
|
||||||
import io.legado.app.domain.gateway.BackupRestoreGateway
|
|
||||||
import io.legado.app.domain.gateway.AiArtifactGateway
|
import io.legado.app.domain.gateway.AiArtifactGateway
|
||||||
import io.legado.app.domain.gateway.AiChatGateway
|
import io.legado.app.domain.gateway.AiChatGateway
|
||||||
import io.legado.app.domain.gateway.AiMemoryGateway
|
import io.legado.app.domain.gateway.AiMemoryGateway
|
||||||
import io.legado.app.domain.gateway.AiProfileGateway
|
import io.legado.app.domain.gateway.AiProfileGateway
|
||||||
import io.legado.app.domain.gateway.AiTextGateway
|
import io.legado.app.domain.gateway.AiTextGateway
|
||||||
import io.legado.app.domain.gateway.AiToolGateway
|
import io.legado.app.domain.gateway.AiToolGateway
|
||||||
|
import io.legado.app.domain.gateway.AppStartupGateway
|
||||||
|
import io.legado.app.domain.gateway.BackupRestoreGateway
|
||||||
import io.legado.app.domain.gateway.BookCacheCleanupGateway
|
import io.legado.app.domain.gateway.BookCacheCleanupGateway
|
||||||
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
import io.legado.app.domain.gateway.BookCacheDownloadGateway
|
||||||
import io.legado.app.domain.gateway.BookSearchGateway
|
import io.legado.app.domain.gateway.BookSearchGateway
|
||||||
@@ -62,6 +63,7 @@ import io.legado.app.domain.gateway.BookSourceCallbackGateway
|
|||||||
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
|
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
|
||||||
import io.legado.app.domain.gateway.DictionaryGateway
|
import io.legado.app.domain.gateway.DictionaryGateway
|
||||||
import io.legado.app.domain.gateway.ExploreBooksGateway
|
import io.legado.app.domain.gateway.ExploreBooksGateway
|
||||||
|
import io.legado.app.domain.gateway.HomeDashboardGateway
|
||||||
import io.legado.app.domain.gateway.HomepageModulesGateway
|
import io.legado.app.domain.gateway.HomepageModulesGateway
|
||||||
import io.legado.app.domain.gateway.LocalBookGateway
|
import io.legado.app.domain.gateway.LocalBookGateway
|
||||||
import io.legado.app.domain.gateway.ReadingProgressGateway
|
import io.legado.app.domain.gateway.ReadingProgressGateway
|
||||||
@@ -70,6 +72,7 @@ import io.legado.app.domain.gateway.WebDavBackupGateway
|
|||||||
import io.legado.app.domain.repository.BookDomainRepository
|
import io.legado.app.domain.repository.BookDomainRepository
|
||||||
import io.legado.app.domain.usecase.AddBookUseCase
|
import io.legado.app.domain.usecase.AddBookUseCase
|
||||||
import io.legado.app.domain.usecase.AddToBookshelfUseCase
|
import io.legado.app.domain.usecase.AddToBookshelfUseCase
|
||||||
|
import io.legado.app.domain.usecase.AiChatGenerationUseCase
|
||||||
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
||||||
import io.legado.app.domain.usecase.BackupRestoreUseCase
|
import io.legado.app.domain.usecase.BackupRestoreUseCase
|
||||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||||
@@ -81,10 +84,10 @@ import io.legado.app.domain.usecase.DeleteBooksUseCase
|
|||||||
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
||||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||||
import io.legado.app.domain.usecase.ExportBookshelfUseCase
|
import io.legado.app.domain.usecase.ExportBookshelfUseCase
|
||||||
|
import io.legado.app.domain.usecase.GenerateChapterSummaryUseCase
|
||||||
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
||||||
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
||||||
import io.legado.app.domain.usecase.AiChatGenerationUseCase
|
import io.legado.app.domain.usecase.HomeDashboardUseCase
|
||||||
import io.legado.app.domain.usecase.GenerateChapterSummaryUseCase
|
|
||||||
import io.legado.app.domain.usecase.ImportBookshelfUseCase
|
import io.legado.app.domain.usecase.ImportBookshelfUseCase
|
||||||
import io.legado.app.domain.usecase.RefreshTocUseCase
|
import io.legado.app.domain.usecase.RefreshTocUseCase
|
||||||
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
|
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
|
||||||
@@ -102,6 +105,7 @@ import io.legado.app.help.coil.CoverInterceptor
|
|||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.okHttpClientManga
|
import io.legado.app.help.http.okHttpClientManga
|
||||||
import io.legado.app.ui.about.AboutViewModel
|
import io.legado.app.ui.about.AboutViewModel
|
||||||
|
import io.legado.app.ui.ai.chat.AiChatViewModel
|
||||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||||
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||||
@@ -124,11 +128,10 @@ import io.legado.app.ui.book.search.SearchViewModel
|
|||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
import io.legado.app.ui.book.toc.TocViewModel
|
import io.legado.app.ui.book.toc.TocViewModel
|
||||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
||||||
import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
|
|
||||||
import io.legado.app.ui.config.ai.AiConfigViewModel
|
import io.legado.app.ui.config.ai.AiConfigViewModel
|
||||||
import io.legado.app.ui.config.ai.AiModelEditViewModel
|
import io.legado.app.ui.config.ai.AiModelEditViewModel
|
||||||
import io.legado.app.ui.config.ai.AiProviderEditViewModel
|
import io.legado.app.ui.config.ai.AiProviderEditViewModel
|
||||||
import io.legado.app.ui.ai.chat.AiChatViewModel
|
import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
|
||||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
|
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
|
||||||
import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigViewModel
|
import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigViewModel
|
||||||
@@ -138,7 +141,6 @@ import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
|
|||||||
import io.legado.app.ui.dict.DictViewModel
|
import io.legado.app.ui.dict.DictViewModel
|
||||||
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
||||||
import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
|
import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
|
||||||
import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
|
|
||||||
import io.legado.app.ui.main.MainRouteSearchContent
|
import io.legado.app.ui.main.MainRouteSearchContent
|
||||||
import io.legado.app.ui.main.MainViewModel
|
import io.legado.app.ui.main.MainViewModel
|
||||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||||
@@ -156,10 +158,12 @@ import io.legado.app.ui.rss.favorites.RssFavoritesViewModel
|
|||||||
import io.legado.app.ui.rss.read.ReadRssViewModel
|
import io.legado.app.ui.rss.read.ReadRssViewModel
|
||||||
import io.legado.app.ui.rss.source.manage.RssSourceViewModel
|
import io.legado.app.ui.rss.source.manage.RssSourceViewModel
|
||||||
import io.legado.app.ui.rss.subscription.RuleSubViewModel
|
import io.legado.app.ui.rss.subscription.RuleSubViewModel
|
||||||
|
import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.core.module.dsl.viewModel
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import java.time.Clock
|
||||||
|
|
||||||
val appModule = module {
|
val appModule = module {
|
||||||
|
|
||||||
@@ -170,6 +174,7 @@ val appModule = module {
|
|||||||
single { get<AppDatabase>().bookSourceDao }
|
single { get<AppDatabase>().bookSourceDao }
|
||||||
|
|
||||||
singleOf(::ReadRecordRepository)
|
singleOf(::ReadRecordRepository)
|
||||||
|
single<HomeDashboardGateway> { HomeDashboardRepository(get(), get()) }
|
||||||
singleOf(::BookRepository)
|
singleOf(::BookRepository)
|
||||||
singleOf(::BookGroupRepository)
|
singleOf(::BookGroupRepository)
|
||||||
singleOf(::BookSourceRepository)
|
singleOf(::BookSourceRepository)
|
||||||
@@ -195,6 +200,7 @@ val appModule = module {
|
|||||||
singleOf(::ClearBookCacheUseCase)
|
singleOf(::ClearBookCacheUseCase)
|
||||||
singleOf(::DeleteBooksUseCase)
|
singleOf(::DeleteBooksUseCase)
|
||||||
singleOf(::GetReadingProgressUseCase)
|
singleOf(::GetReadingProgressUseCase)
|
||||||
|
single { HomeDashboardUseCase(get(), Clock.systemDefaultZone()) }
|
||||||
singleOf(::RemoveBookGroupAssignmentUseCase)
|
singleOf(::RemoveBookGroupAssignmentUseCase)
|
||||||
singleOf(::UpdateBooksGroupUseCase)
|
singleOf(::UpdateBooksGroupUseCase)
|
||||||
singleOf(::UploadReadingProgressUseCase)
|
singleOf(::UploadReadingProgressUseCase)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package io.legado.app.domain.gateway
|
||||||
|
|
||||||
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
|
import io.legado.app.domain.model.HomeReadingBook
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface HomeDashboardGateway {
|
||||||
|
fun observeTotalReadBooks(): Flow<Int>
|
||||||
|
|
||||||
|
fun observeTotalReadTime(): Flow<Long>
|
||||||
|
|
||||||
|
fun observeReadTime(date: String): Flow<Long>
|
||||||
|
|
||||||
|
fun observeRecentBooks(limit: Int): Flow<List<HomeReadingBook>>
|
||||||
|
|
||||||
|
fun observeDailyGoal(defaultValue: Int): Flow<Int>
|
||||||
|
|
||||||
|
fun observeSelectedSourceSetUrl(): Flow<String?>
|
||||||
|
|
||||||
|
fun observeVisibleSections(): Flow<Set<HomeDashboardSection>>
|
||||||
|
|
||||||
|
suspend fun updateDailyGoal(minutes: Int)
|
||||||
|
|
||||||
|
suspend fun updateSelectedSourceSetUrl(sourceUrl: String)
|
||||||
|
|
||||||
|
suspend fun updateVisibleSections(sections: Set<HomeDashboardSection>)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package io.legado.app.domain.model
|
||||||
|
|
||||||
|
data class HomeDashboard(
|
||||||
|
val totalReadBooks: Int,
|
||||||
|
val totalReadTimeMillis: Long,
|
||||||
|
val todayReadTimeMillis: Long,
|
||||||
|
val dailyGoalMinutes: Int,
|
||||||
|
val recentBooks: List<HomeReadingBook>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HomeReadingBook(
|
||||||
|
val bookUrl: String?,
|
||||||
|
val name: String,
|
||||||
|
val author: String,
|
||||||
|
val origin: String?,
|
||||||
|
val coverPath: String?,
|
||||||
|
val chapterTitle: String?,
|
||||||
|
val chapterProgress: Float?,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class HomeDashboardSection(val storageValue: String) {
|
||||||
|
RecentBook("recent_book"),
|
||||||
|
TotalReadBooks("total_read_books"),
|
||||||
|
TotalReadTime("total_read_time"),
|
||||||
|
RecentBooks("recent_books"),
|
||||||
|
DailyGoal("daily_goal"),
|
||||||
|
WebDavBackup("webdav_backup");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromStorage(value: String): Set<HomeDashboardSection> {
|
||||||
|
if (value.isBlank()) return emptySet()
|
||||||
|
val sections = value
|
||||||
|
.split(',')
|
||||||
|
.map(String::trim)
|
||||||
|
.mapNotNull { stored ->
|
||||||
|
entries.firstOrNull { it.storageValue == stored }
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
return sections.ifEmpty { entries.toSet() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val DEFAULT_HOME_DASHBOARD_SECTIONS: Set<HomeDashboardSection> =
|
||||||
|
HomeDashboardSection.entries.toSet()
|
||||||
|
|
||||||
|
const val DEFAULT_DAILY_READING_GOAL_MINUTES = 30
|
||||||
|
const val MAX_DAILY_READING_GOAL_MINUTES = 24 * 60
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package io.legado.app.domain.usecase
|
||||||
|
|
||||||
|
import io.legado.app.domain.gateway.HomeDashboardGateway
|
||||||
|
import io.legado.app.domain.model.DEFAULT_DAILY_READING_GOAL_MINUTES
|
||||||
|
import io.legado.app.domain.model.HomeDashboard
|
||||||
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
|
import io.legado.app.domain.model.MAX_DAILY_READING_GOAL_MINUTES
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
class HomeDashboardUseCase(
|
||||||
|
private val gateway: HomeDashboardGateway,
|
||||||
|
private val clock: Clock,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun observe(): Flow<HomeDashboard> {
|
||||||
|
val todayReadTime = currentDate()
|
||||||
|
.flatMapLatest(gateway::observeReadTime)
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
gateway.observeTotalReadBooks(),
|
||||||
|
gateway.observeTotalReadTime(),
|
||||||
|
todayReadTime,
|
||||||
|
gateway.observeRecentBooks(RECENT_BOOK_LIMIT),
|
||||||
|
gateway.observeDailyGoal(DEFAULT_DAILY_READING_GOAL_MINUTES),
|
||||||
|
) { totalBooks, totalTime, todayTime, recentBooks, dailyGoal ->
|
||||||
|
HomeDashboard(
|
||||||
|
totalReadBooks = totalBooks,
|
||||||
|
totalReadTimeMillis = totalTime,
|
||||||
|
todayReadTimeMillis = todayTime,
|
||||||
|
dailyGoalMinutes = dailyGoal.coerceIn(
|
||||||
|
1,
|
||||||
|
MAX_DAILY_READING_GOAL_MINUTES,
|
||||||
|
),
|
||||||
|
recentBooks = recentBooks,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateDailyGoal(minutes: Int) {
|
||||||
|
gateway.updateDailyGoal(
|
||||||
|
minutes.coerceIn(1, MAX_DAILY_READING_GOAL_MINUTES)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeSelectedSourceSetUrl(): Flow<String?> =
|
||||||
|
gateway.observeSelectedSourceSetUrl()
|
||||||
|
|
||||||
|
fun observeVisibleSections(): Flow<Set<HomeDashboardSection>> =
|
||||||
|
gateway.observeVisibleSections()
|
||||||
|
|
||||||
|
suspend fun updateSelectedSourceSetUrl(sourceUrl: String) {
|
||||||
|
gateway.updateSelectedSourceSetUrl(sourceUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateVisibleSections(sections: Set<HomeDashboardSection>) {
|
||||||
|
gateway.updateVisibleSections(sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentDate(): Flow<String> = flow {
|
||||||
|
while (true) {
|
||||||
|
val now = ZonedDateTime.now(clock)
|
||||||
|
emit(now.toLocalDate().toString())
|
||||||
|
val nextDay = now.toLocalDate()
|
||||||
|
.plusDays(1)
|
||||||
|
.atStartOfDay(clock.zone)
|
||||||
|
delay(
|
||||||
|
Duration.between(now, nextDay)
|
||||||
|
.toMillis()
|
||||||
|
.coerceAtLeast(MIN_DATE_REFRESH_DELAY_MILLIS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val RECENT_BOOK_LIMIT = 7
|
||||||
|
const val MIN_DATE_REFRESH_DELAY_MILLIS = 1_000L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -314,6 +314,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
BackupConfig.backupPath = value
|
BackupConfig.backupPath = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showHome: Boolean
|
||||||
|
get() = ThemeConfig.showHome
|
||||||
|
|
||||||
val showDiscovery: Boolean
|
val showDiscovery: Boolean
|
||||||
get() = ThemeConfig.showDiscovery
|
get() = ThemeConfig.showDiscovery
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ object ThemeImportExport {
|
|||||||
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
showHome = ThemeConfig.showHome,
|
||||||
showDiscovery = ThemeConfig.showDiscovery,
|
showDiscovery = ThemeConfig.showDiscovery,
|
||||||
showRss = ThemeConfig.showRss,
|
showRss = ThemeConfig.showRss,
|
||||||
showStatusBar = ThemeConfig.showStatusBar,
|
showStatusBar = ThemeConfig.showStatusBar,
|
||||||
@@ -184,6 +185,7 @@ object ThemeImportExport {
|
|||||||
tabletInterface = ThemeConfig.tabletInterface,
|
tabletInterface = ThemeConfig.tabletInterface,
|
||||||
labelVisibilityMode = ThemeConfig.labelVisibilityMode,
|
labelVisibilityMode = ThemeConfig.labelVisibilityMode,
|
||||||
defaultHomePage = ThemeConfig.defaultHomePage,
|
defaultHomePage = ThemeConfig.defaultHomePage,
|
||||||
|
mainNavigationOrder = ThemeConfig.mainNavigationOrder,
|
||||||
|
|
||||||
// 导航栏图标
|
// 导航栏图标
|
||||||
navIconHome = ThemeConfig.navIconHome,
|
navIconHome = ThemeConfig.navIconHome,
|
||||||
@@ -343,6 +345,7 @@ object ThemeImportExport {
|
|||||||
ThemeConfig.customTagColorsJson = data.customTagColorsJson
|
ThemeConfig.customTagColorsJson = data.customTagColorsJson
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
ThemeConfig.showHome = data.showHome
|
||||||
ThemeConfig.showDiscovery = data.showDiscovery
|
ThemeConfig.showDiscovery = data.showDiscovery
|
||||||
ThemeConfig.showRss = data.showRss
|
ThemeConfig.showRss = data.showRss
|
||||||
ThemeConfig.showStatusBar = data.showStatusBar
|
ThemeConfig.showStatusBar = data.showStatusBar
|
||||||
@@ -353,6 +356,7 @@ object ThemeImportExport {
|
|||||||
ThemeConfig.tabletInterface = data.tabletInterface
|
ThemeConfig.tabletInterface = data.tabletInterface
|
||||||
ThemeConfig.labelVisibilityMode = data.labelVisibilityMode
|
ThemeConfig.labelVisibilityMode = data.labelVisibilityMode
|
||||||
ThemeConfig.defaultHomePage = data.defaultHomePage
|
ThemeConfig.defaultHomePage = data.defaultHomePage
|
||||||
|
ThemeConfig.mainNavigationOrder = data.mainNavigationOrder
|
||||||
|
|
||||||
// 导航栏图标
|
// 导航栏图标
|
||||||
ThemeConfig.navIconHome = data.navIconHome
|
ThemeConfig.navIconHome = data.navIconHome
|
||||||
@@ -579,6 +583,7 @@ data class ThemeExportData(
|
|||||||
val customTagColorsJson: String? = null,
|
val customTagColorsJson: String? = null,
|
||||||
|
|
||||||
// 主界面设置
|
// 主界面设置
|
||||||
|
val showHome: Boolean = true,
|
||||||
val showDiscovery: Boolean = true,
|
val showDiscovery: Boolean = true,
|
||||||
val showRss: Boolean = true,
|
val showRss: Boolean = true,
|
||||||
val showStatusBar: Boolean = true,
|
val showStatusBar: Boolean = true,
|
||||||
@@ -589,6 +594,7 @@ data class ThemeExportData(
|
|||||||
val tabletInterface: String = "auto",
|
val tabletInterface: String = "auto",
|
||||||
val labelVisibilityMode: String = "auto",
|
val labelVisibilityMode: String = "auto",
|
||||||
val defaultHomePage: String = "bookshelf",
|
val defaultHomePage: String = "bookshelf",
|
||||||
|
val mainNavigationOrder: String = "home,bookshelf,explore,rss,my",
|
||||||
|
|
||||||
// 导航栏图标
|
// 导航栏图标
|
||||||
val navIconHome: String = "",
|
val navIconHome: String = "",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.legado.app.constant.PreferKey
|
|||||||
import io.legado.app.ui.config.prefDelegate
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
|
||||||
object MainConfig {
|
object MainConfig {
|
||||||
|
var showHome by prefDelegate(PreferKey.showHome, true)
|
||||||
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
var showRSS by prefDelegate(PreferKey.showRss, true)
|
var showRSS by prefDelegate(PreferKey.showRss, true)
|
||||||
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
||||||
@@ -13,6 +14,10 @@ object MainConfig {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||||
|
var mainNavigationOrder by prefDelegate(
|
||||||
|
PreferKey.mainNavigationOrder,
|
||||||
|
"home,bookshelf,explore,rss,my",
|
||||||
|
)
|
||||||
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
||||||
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
||||||
var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
|
var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.main.MainDestination
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.utils.move
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainNavigationSettingsSheet(
|
||||||
|
show: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
val homeLabel = stringResource(R.string.home)
|
||||||
|
val bookshelfLabel = stringResource(R.string.bookshelf)
|
||||||
|
val discoveryLabel = stringResource(R.string.discovery)
|
||||||
|
val rssLabel = stringResource(R.string.rss)
|
||||||
|
val myLabel = stringResource(R.string.my)
|
||||||
|
val destinations = listOfNotNull(
|
||||||
|
if (ThemeConfig.showHome) homeLabel to MainDestination.Home.route else null,
|
||||||
|
bookshelfLabel to MainDestination.Bookshelf.route,
|
||||||
|
if (ThemeConfig.showDiscovery) {
|
||||||
|
discoveryLabel to MainDestination.Explore.route
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
if (ThemeConfig.showRss) rssLabel to MainDestination.Rss.route else null,
|
||||||
|
myLabel to MainDestination.My.route,
|
||||||
|
)
|
||||||
|
val selectedDefault = ThemeConfig.defaultHomePage.takeIf { route ->
|
||||||
|
destinations.any { it.second == route }
|
||||||
|
} ?: MainDestination.Bookshelf.route
|
||||||
|
val configuredOrder = MainDestination.ordered(ThemeConfig.mainNavigationOrder)
|
||||||
|
var navigationItems by remember(show) { mutableStateOf(configuredOrder) }
|
||||||
|
val navigationListState = rememberLazyListState()
|
||||||
|
val reorderableState =
|
||||||
|
rememberReorderableLazyListState(navigationListState) { from, to ->
|
||||||
|
navigationItems = navigationItems.toMutableList().apply {
|
||||||
|
move(from.index, to.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(configuredOrder, reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
navigationItems = configuredOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
ThemeConfig.mainNavigationOrder =
|
||||||
|
navigationItems.joinToString(",") { it.route }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateVisibility(
|
||||||
|
route: String,
|
||||||
|
visible: Boolean,
|
||||||
|
update: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
update(visible)
|
||||||
|
if (!visible && ThemeConfig.defaultHomePage == route) {
|
||||||
|
ThemeConfig.defaultHomePage = MainDestination.Bookshelf.route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = stringResource(R.string.main_navigation_settings),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 24.dp),
|
||||||
|
) {
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_home),
|
||||||
|
checked = ThemeConfig.showHome,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateVisibility(
|
||||||
|
MainDestination.Home.route,
|
||||||
|
it,
|
||||||
|
) { value -> ThemeConfig.showHome = value }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_discovery),
|
||||||
|
checked = ThemeConfig.showDiscovery,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateVisibility(
|
||||||
|
MainDestination.Explore.route,
|
||||||
|
it,
|
||||||
|
) { value -> ThemeConfig.showDiscovery = value }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_rss),
|
||||||
|
checked = ThemeConfig.showRss,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateVisibility(
|
||||||
|
MainDestination.Rss.route,
|
||||||
|
it,
|
||||||
|
) { value -> ThemeConfig.showRss = value }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = stringResource(R.string.default_home_page),
|
||||||
|
selectedValue = selectedDefault,
|
||||||
|
displayEntries = destinations.map { it.first }.toTypedArray(),
|
||||||
|
entryValues = destinations.map { it.second }.toTypedArray(),
|
||||||
|
onValueChange = { ThemeConfig.defaultHomePage = it },
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.navigation_order),
|
||||||
|
style = LegadoTheme.typography.titleSmallEmphasized,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
top = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 8.dp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
state = navigationListState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 360.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = navigationItems,
|
||||||
|
key = { it.route },
|
||||||
|
) { destination ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = destination.route,
|
||||||
|
title = stringResource(destination.labelId),
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,8 @@ object ThemeConfig {
|
|||||||
customTagColorsJson = GSON.toJson(colors)
|
customTagColorsJson = GSON.toJson(colors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showHome by prefDelegate(PreferKey.showHome, true)
|
||||||
|
|
||||||
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
|
|
||||||
var showRss by prefDelegate(PreferKey.showRss, true)
|
var showRss by prefDelegate(PreferKey.showRss, true)
|
||||||
@@ -165,6 +167,11 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||||
|
|
||||||
|
var mainNavigationOrder by prefDelegate(
|
||||||
|
PreferKey.mainNavigationOrder,
|
||||||
|
"home,bookshelf,explore,rss,my",
|
||||||
|
)
|
||||||
|
|
||||||
var navExtended by prefDelegate("navExtended", false)
|
var navExtended by prefDelegate("navExtended", false)
|
||||||
|
|
||||||
var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
|
var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ fun ThemeConfigScreen(
|
|||||||
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
||||||
var showBorderColorPicker by remember { mutableStateOf(false) }
|
var showBorderColorPicker by remember { mutableStateOf(false) }
|
||||||
var showNavIconSheet by remember { mutableStateOf(false) }
|
var showNavIconSheet by remember { mutableStateOf(false) }
|
||||||
|
var showMainNavigationSheet by remember { mutableStateOf(false) }
|
||||||
var showFontSheet by remember { mutableStateOf(false) }
|
var showFontSheet by remember { mutableStateOf(false) }
|
||||||
val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
|
val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -375,15 +376,10 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
||||||
SwitchSettingItem(
|
ClickableSettingItem(
|
||||||
title = stringResource(R.string.show_discovery),
|
title = stringResource(R.string.main_navigation_settings),
|
||||||
checked = ThemeConfig.showDiscovery,
|
description = stringResource(R.string.main_navigation_settings_summary),
|
||||||
onCheckedChange = { ThemeConfig.showDiscovery = it }
|
onClick = { showMainNavigationSheet = true },
|
||||||
)
|
|
||||||
SwitchSettingItem(
|
|
||||||
title = stringResource(R.string.show_rss),
|
|
||||||
checked = ThemeConfig.showRss,
|
|
||||||
onCheckedChange = { ThemeConfig.showRss = it }
|
|
||||||
)
|
)
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.show_status),
|
title = stringResource(R.string.show_status),
|
||||||
@@ -446,13 +442,6 @@ fun ThemeConfigScreen(
|
|||||||
entryValues = stringArrayResource(R.array.label_vis_mode_value),
|
entryValues = stringArrayResource(R.array.label_vis_mode_value),
|
||||||
onValueChange = { ThemeConfig.labelVisibilityMode = it }
|
onValueChange = { ThemeConfig.labelVisibilityMode = it }
|
||||||
)
|
)
|
||||||
DropdownListSettingItem(
|
|
||||||
title = stringResource(R.string.default_home_page),
|
|
||||||
selectedValue = ThemeConfig.defaultHomePage,
|
|
||||||
displayEntries = stringArrayResource(R.array.default_home_page),
|
|
||||||
entryValues = stringArrayResource(R.array.default_home_page_value),
|
|
||||||
onValueChange = { ThemeConfig.defaultHomePage = it }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.eye_protection)) {
|
SplicedColumnGroup(title = stringResource(R.string.eye_protection)) {
|
||||||
@@ -841,6 +830,11 @@ fun ThemeConfigScreen(
|
|||||||
onDismissRequest = { showNavIconSheet = false }
|
onDismissRequest = { showNavIconSheet = false }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MainNavigationSettingsSheet(
|
||||||
|
show = showMainNavigationSheet,
|
||||||
|
onDismissRequest = { showMainNavigationSheet = false },
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
LauncherIconPickerSheet(
|
LauncherIconPickerSheet(
|
||||||
show = showLauncherIconPicker,
|
show = showLauncherIconPicker,
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ fun EditThemeSheet(
|
|||||||
|
|
||||||
// Interface layout
|
// Interface layout
|
||||||
SectionTitle(stringResource(R.string.theme_manage_section_layout))
|
SectionTitle(stringResource(R.string.theme_manage_section_layout))
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_home),
|
||||||
|
checked = data.showHome,
|
||||||
|
onCheckedChange = { data = data.copy(showHome = it) }
|
||||||
|
)
|
||||||
CompactSwitchSettingItem(
|
CompactSwitchSettingItem(
|
||||||
title = stringResource(R.string.theme_manage_show_discovery),
|
title = stringResource(R.string.theme_manage_show_discovery),
|
||||||
checked = data.showDiscovery,
|
checked = data.showDiscovery,
|
||||||
|
|||||||
@@ -35,9 +35,17 @@ sealed class MainDestination(
|
|||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DISCOVERY_MODULES_ROUTE = "explore_modules"
|
|
||||||
|
|
||||||
val mainDestinations = persistentListOf<MainDestination>(Home, Bookshelf, Explore, Rss, My)
|
val mainDestinations = persistentListOf<MainDestination>(Home, Bookshelf, Explore, Rss, My)
|
||||||
|
|
||||||
|
fun ordered(order: String): List<MainDestination> {
|
||||||
|
val byRoute = mainDestinations.associateBy { it.route }
|
||||||
|
val ordered = order
|
||||||
|
.split(',')
|
||||||
|
.map(String::trim)
|
||||||
|
.distinct()
|
||||||
|
.mapNotNull(byRoute::get)
|
||||||
|
return ordered + mainDestinations.filterNot { it in ordered }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,14 +182,9 @@ fun MainScreen(
|
|||||||
val destinations = mainUiState.destinations
|
val destinations = mainUiState.destinations
|
||||||
|
|
||||||
val initialPage = remember(destinations, mainUiState.defaultHomePage) {
|
val initialPage = remember(destinations, mainUiState.defaultHomePage) {
|
||||||
val defaultRoute = if (
|
val index = destinations.indexOfFirst {
|
||||||
mainUiState.defaultHomePage == MainDestination.DISCOVERY_MODULES_ROUTE
|
it.route == mainUiState.defaultHomePage
|
||||||
) {
|
|
||||||
MainDestination.Explore.route
|
|
||||||
} else {
|
|
||||||
mainUiState.defaultHomePage
|
|
||||||
}
|
}
|
||||||
val index = destinations.indexOfFirst { it.route == defaultRoute }
|
|
||||||
if (index != -1) index else 0
|
if (index != -1) index else 0
|
||||||
}
|
}
|
||||||
val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size }
|
val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size }
|
||||||
@@ -392,6 +387,17 @@ fun MainScreen(
|
|||||||
onOpenBook = { book ->
|
onOpenBook = { book ->
|
||||||
context.startActivityForBook(book)
|
context.startActivityForBook(book)
|
||||||
},
|
},
|
||||||
|
onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
||||||
|
onNavigateToBookInfo(
|
||||||
|
name ?: "",
|
||||||
|
author ?: "",
|
||||||
|
bookUrl,
|
||||||
|
origin,
|
||||||
|
coverPath,
|
||||||
|
sharedCoverKey,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOpenExploreShow = onNavigateToExploreShow,
|
||||||
onOpenBackupSettings = onNavigateToBackupSettings,
|
onOpenBackupSettings = onNavigateToBackupSettings,
|
||||||
onNavigateToReadRecord = onNavigateToReadRecord,
|
onNavigateToReadRecord = onNavigateToReadRecord,
|
||||||
onNavigateToReadRecordOverview = onNavigateToReadRecordOverview,
|
onNavigateToReadRecordOverview = onNavigateToReadRecordOverview,
|
||||||
@@ -423,21 +429,7 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
MainDestination.Explore -> ExploreScreen(
|
MainDestination.Explore -> ExploreScreen(
|
||||||
style = mainUiState.exploreStyle,
|
|
||||||
onStyleChange = viewModel::setExploreStyle,
|
|
||||||
onBookClick = { name, author, bookUrl, origin, coverPath, sharedCoverKey ->
|
|
||||||
onNavigateToBookInfo(
|
|
||||||
name ?: "",
|
|
||||||
author ?: "",
|
|
||||||
bookUrl,
|
|
||||||
origin,
|
|
||||||
coverPath,
|
|
||||||
sharedCoverKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onOpenExploreShow = onNavigateToExploreShow,
|
onOpenExploreShow = onNavigateToExploreShow,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
)
|
)
|
||||||
MainDestination.Rss -> RssScreen(
|
MainDestination.Rss -> RssScreen(
|
||||||
onOpenSort = { sourceUrl, sortUrl, key ->
|
onOpenSort = { sourceUrl, sortUrl, key ->
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
|
||||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
|
||||||
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
||||||
import io.legado.app.domain.usecase.WebDavBackupUseCase
|
import io.legado.app.domain.usecase.WebDavBackupUseCase
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.main.explore.ExploreStyle
|
|
||||||
import io.legado.app.ui.main.my.PrefClickEvent
|
import io.legado.app.ui.main.my.PrefClickEvent
|
||||||
import io.legado.app.utils.eventBus.FlowEventBus
|
import io.legado.app.utils.eventBus.FlowEventBus
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -27,7 +24,6 @@ class MainViewModel(
|
|||||||
application: Application,
|
application: Application,
|
||||||
private val appStartupMaintenanceUseCase: AppStartupMaintenanceUseCase,
|
private val appStartupMaintenanceUseCase: AppStartupMaintenanceUseCase,
|
||||||
private val webDavBackupUseCase: WebDavBackupUseCase,
|
private val webDavBackupUseCase: WebDavBackupUseCase,
|
||||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(readMainUiState())
|
private val _uiState = MutableStateFlow(readMainUiState())
|
||||||
@@ -36,24 +32,12 @@ class MainViewModel(
|
|||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// 通过 snapshotFlow 直接观察 ThemeConfig 的 Compose State,全链路走 DS
|
// 通过 snapshotFlow 直接观察 ThemeConfig 的 Compose State
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
readMainUiState()
|
readMainUiState()
|
||||||
}.collect { newState ->
|
}.collect { newState ->
|
||||||
_uiState.update { currentState ->
|
_uiState.value = newState
|
||||||
newState.copy(exploreStyle = currentState.exploreStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
|
||||||
localPreferencesRepository.getPreference(
|
|
||||||
LocalPreferencesKeys.EXPLORE_STYLE,
|
|
||||||
ExploreStyle.ClassicDiscovery.storageValue,
|
|
||||||
).collect { storedValue ->
|
|
||||||
_uiState.update {
|
|
||||||
it.copy(exploreStyle = ExploreStyle.fromStorageValue(storedValue))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteNotShelfBook()
|
deleteNotShelfBook()
|
||||||
@@ -89,17 +73,6 @@ class MainViewModel(
|
|||||||
ThemeConfig.navExtended = expanded
|
ThemeConfig.navExtended = expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setExploreStyle(style: ExploreStyle) {
|
|
||||||
if (_uiState.value.exploreStyle == style) return
|
|
||||||
_uiState.update { it.copy(exploreStyle = style) }
|
|
||||||
viewModelScope.launch {
|
|
||||||
localPreferencesRepository.updatePreference(
|
|
||||||
LocalPreferencesKeys.EXPLORE_STYLE,
|
|
||||||
style.storageValue,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPrefClickEvent(event: PrefClickEvent) {
|
fun onPrefClickEvent(event: PrefClickEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PrefClickEvent.OpenUrl -> _effects.tryEmit(MainEffect.OpenUrl(event.url))
|
is PrefClickEvent.OpenUrl -> _effects.tryEmit(MainEffect.OpenUrl(event.url))
|
||||||
@@ -158,12 +131,12 @@ data class MainUiState(
|
|||||||
val useFloatingBottomBarLiquidGlass: Boolean = false,
|
val useFloatingBottomBarLiquidGlass: Boolean = false,
|
||||||
val labelVisibilityMode: String = "auto",
|
val labelVisibilityMode: String = "auto",
|
||||||
val navExtended: Boolean = false,
|
val navExtended: Boolean = false,
|
||||||
val exploreStyle: ExploreStyle = ExploreStyle.ClassicDiscovery,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun MainViewModel.readMainUiState(): MainUiState {
|
private fun MainViewModel.readMainUiState(): MainUiState {
|
||||||
val destinations = MainDestination.mainDestinations.filter {
|
val destinations = MainDestination.ordered(ThemeConfig.mainNavigationOrder).filter {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
MainDestination.Home -> ThemeConfig.showHome
|
||||||
MainDestination.Explore -> ThemeConfig.showDiscovery
|
MainDestination.Explore -> ThemeConfig.showDiscovery
|
||||||
MainDestination.Rss -> ThemeConfig.showRss
|
MainDestination.Rss -> ThemeConfig.showRss
|
||||||
else -> true
|
else -> true
|
||||||
|
|||||||
@@ -2,16 +2,10 @@ package io.legado.app.ui.main.explore
|
|||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -32,7 +26,6 @@ import androidx.compose.material.icons.filled.ChevronRight
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Group
|
import androidx.compose.material.icons.filled.Group
|
||||||
import androidx.compose.material.icons.filled.Home
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.VerticalAlignTop
|
import androidx.compose.material.icons.filled.VerticalAlignTop
|
||||||
@@ -47,7 +40,6 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -67,7 +59,6 @@ import io.legado.app.ui.book.search.SearchActivity
|
|||||||
import io.legado.app.ui.book.search.SearchScope
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
import io.legado.app.ui.main.homepage.HomepageScreen
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
@@ -95,64 +86,19 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreScreen(
|
fun ExploreScreen(
|
||||||
style: ExploreStyle,
|
|
||||||
onStyleChange: (ExploreStyle) -> Unit,
|
|
||||||
onBookClick: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
|
|
||||||
onOpenExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
onOpenExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
|
||||||
) {
|
) {
|
||||||
val stateHolder = rememberSaveableStateHolder()
|
ExploreDiscoveryScreen(onOpenExploreShow = onOpenExploreShow)
|
||||||
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = style,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 220,
|
|
||||||
delayMillis = 90,
|
|
||||||
)
|
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(durationMillis = 90)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentKey = { it },
|
|
||||||
label = "ExploreStyle",
|
|
||||||
) { page ->
|
|
||||||
stateHolder.SaveableStateProvider(page) {
|
|
||||||
when (page) {
|
|
||||||
ExploreStyle.DiscoveryModules -> HomepageScreen(
|
|
||||||
onBookClick = onBookClick,
|
|
||||||
onModuleHeaderClick = onOpenExploreShow,
|
|
||||||
onSwitchToDiscovery = {
|
|
||||||
onStyleChange(ExploreStyle.ClassicDiscovery)
|
|
||||||
},
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> ExploreDiscoveryScreen(
|
|
||||||
onOpenExploreShow = onOpenExploreShow,
|
|
||||||
onSwitchToHomepage = {
|
|
||||||
onStyleChange(ExploreStyle.DiscoveryModules)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalFoundationApi::class,
|
ExperimentalFoundationApi::class,
|
||||||
ExperimentalSharedTransitionApi::class,
|
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExploreDiscoveryScreen(
|
private fun ExploreDiscoveryScreen(
|
||||||
viewModel: ExploreViewModel = koinViewModel(),
|
viewModel: ExploreViewModel = koinViewModel(),
|
||||||
onOpenExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
onOpenExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||||
onSwitchToHomepage: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context as? AppCompatActivity
|
val activity = context as? AppCompatActivity
|
||||||
@@ -208,14 +154,6 @@ private fun ExploreDiscoveryScreen(
|
|||||||
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
||||||
searchPlaceholder = stringResource(R.string.search),
|
searchPlaceholder = stringResource(R.string.search),
|
||||||
dropDownMenuContent = { dismiss ->
|
dropDownMenuContent = { dismiss ->
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.Home) },
|
|
||||||
text = stringResource(R.string.switch_to_discovery_modules),
|
|
||||||
onClick = {
|
|
||||||
dismiss()
|
|
||||||
onSwitchToHomepage()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.Group) },
|
leadingIcon = { MenuItemIcon(Icons.Default.Group) },
|
||||||
text = stringResource(R.string.all),
|
text = stringResource(R.string.all),
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package io.legado.app.ui.main.explore
|
|
||||||
|
|
||||||
enum class ExploreStyle(val storageValue: String) {
|
|
||||||
DiscoveryModules("discovery_modules"),
|
|
||||||
ClassicDiscovery("classic_discovery");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromStorageValue(value: String): ExploreStyle {
|
|
||||||
return entries.firstOrNull { it.storageValue == value } ?: ClassicDiscovery
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,13 @@ package io.legado.app.ui.main.home
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.domain.model.DEFAULT_DAILY_READING_GOAL_MINUTES
|
||||||
|
import io.legado.app.domain.model.DEFAULT_HOME_DASHBOARD_SECTIONS
|
||||||
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class HomeUiState(
|
data class HomeUiState(
|
||||||
@@ -11,8 +18,13 @@ data class HomeUiState(
|
|||||||
val todayReadTimeMillis: Long = 0L,
|
val todayReadTimeMillis: Long = 0L,
|
||||||
val dailyGoalMinutes: Int = DEFAULT_DAILY_READING_GOAL_MINUTES,
|
val dailyGoalMinutes: Int = DEFAULT_DAILY_READING_GOAL_MINUTES,
|
||||||
val recentBook: HomeRecentBookUi? = null,
|
val recentBook: HomeRecentBookUi? = null,
|
||||||
|
val recentBooks: ImmutableList<HomeRecentBookUi> = persistentListOf(),
|
||||||
|
val selectedSourceSetUrl: String? = null,
|
||||||
|
val visibleSections: ImmutableSet<HomeDashboardSection> =
|
||||||
|
DEFAULT_HOME_DASHBOARD_SECTIONS.toImmutableSet(),
|
||||||
val latestBackup: HomeBackupUi? = null,
|
val latestBackup: HomeBackupUi? = null,
|
||||||
val isBackupLoading: Boolean = true,
|
val isBackupLoading: Boolean = true,
|
||||||
|
val isBackupLoadError: Boolean = false,
|
||||||
val isBackupActionRunning: Boolean = false,
|
val isBackupActionRunning: Boolean = false,
|
||||||
val activeDialog: HomeDialog? = null,
|
val activeDialog: HomeDialog? = null,
|
||||||
val activeSheet: HomeSheet? = null,
|
val activeSheet: HomeSheet? = null,
|
||||||
@@ -37,6 +49,13 @@ data class HomeBackupUi(
|
|||||||
|
|
||||||
sealed interface HomeIntent {
|
sealed interface HomeIntent {
|
||||||
data object RecentBookClick : HomeIntent
|
data object RecentBookClick : HomeIntent
|
||||||
|
data class RecentHistoryBookClick(val bookUrl: String) : HomeIntent
|
||||||
|
data class SelectSourceSet(val sourceUrl: String) : HomeIntent
|
||||||
|
data object DashboardSettingsClick : HomeIntent
|
||||||
|
data class SetSectionVisible(
|
||||||
|
val section: HomeDashboardSection,
|
||||||
|
val visible: Boolean,
|
||||||
|
) : HomeIntent
|
||||||
data object ReadingGoalClick : HomeIntent
|
data object ReadingGoalClick : HomeIntent
|
||||||
data class UpdateReadingGoal(val minutes: Int) : HomeIntent
|
data class UpdateReadingGoal(val minutes: Int) : HomeIntent
|
||||||
data object BackupClick : HomeIntent
|
data object BackupClick : HomeIntent
|
||||||
@@ -52,6 +71,7 @@ sealed interface HomeIntent {
|
|||||||
data class RestoreLocalFileSelected(val uri: String) : HomeIntent
|
data class RestoreLocalFileSelected(val uri: String) : HomeIntent
|
||||||
data object ConfirmRestore : HomeIntent
|
data object ConfirmRestore : HomeIntent
|
||||||
data object BackupSettingsClick : HomeIntent
|
data object BackupSettingsClick : HomeIntent
|
||||||
|
data object RetryBackupInfo : HomeIntent
|
||||||
data object DismissDialog : HomeIntent
|
data object DismissDialog : HomeIntent
|
||||||
data object DismissSheet : HomeIntent
|
data object DismissSheet : HomeIntent
|
||||||
}
|
}
|
||||||
@@ -83,6 +103,7 @@ sealed interface HomeDialog {
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
sealed interface HomeSheet {
|
sealed interface HomeSheet {
|
||||||
|
data object DashboardSettings : HomeSheet
|
||||||
data object BackupOptions : HomeSheet
|
data object BackupOptions : HomeSheet
|
||||||
data object RestoreOptions : HomeSheet
|
data object RestoreOptions : HomeSheet
|
||||||
}
|
}
|
||||||
@@ -92,6 +113,3 @@ enum class HomeBackupDestination(val mode: String) {
|
|||||||
WebDav("webdav"),
|
WebDav("webdav"),
|
||||||
LocalAndWebDav("both"),
|
LocalAndWebDav("both"),
|
||||||
}
|
}
|
||||||
|
|
||||||
const val DEFAULT_DAILY_READING_GOAL_MINUTES = 30
|
|
||||||
const val MAX_DAILY_READING_GOAL_MINUTES = 24 * 60
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -3,31 +3,35 @@ package io.legado.app.ui.main.home
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
|
||||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
|
||||||
import io.legado.app.data.repository.BookRepository
|
import io.legado.app.data.repository.BookRepository
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.domain.model.HomeDashboardSection
|
||||||
|
import io.legado.app.domain.model.HomeReadingBook
|
||||||
import io.legado.app.domain.model.WebDavBackup
|
import io.legado.app.domain.model.WebDavBackup
|
||||||
import io.legado.app.domain.usecase.BackupRestoreUseCase
|
import io.legado.app.domain.usecase.BackupRestoreUseCase
|
||||||
|
import io.legado.app.domain.usecase.HomeDashboardUseCase
|
||||||
import io.legado.app.domain.usecase.WebDavBackupUseCase
|
import io.legado.app.domain.usecase.WebDavBackupUseCase
|
||||||
import io.legado.app.ui.config.backupConfig.BackupConfig
|
import io.legado.app.ui.config.backupConfig.BackupConfig
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDate
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val readRecordRepository: ReadRecordRepository,
|
private val homeDashboardUseCase: HomeDashboardUseCase,
|
||||||
private val bookRepository: BookRepository,
|
private val bookRepository: BookRepository,
|
||||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
|
||||||
private val webDavBackupUseCase: WebDavBackupUseCase,
|
private val webDavBackupUseCase: WebDavBackupUseCase,
|
||||||
private val backupRestoreUseCase: BackupRestoreUseCase,
|
private val backupRestoreUseCase: BackupRestoreUseCase,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -37,58 +41,15 @@ class HomeViewModel(
|
|||||||
private val _activeSheet = MutableStateFlow<HomeSheet?>(null)
|
private val _activeSheet = MutableStateFlow<HomeSheet?>(null)
|
||||||
private val _effects = MutableSharedFlow<HomeEffect>(extraBufferCapacity = 16)
|
private val _effects = MutableSharedFlow<HomeEffect>(extraBufferCapacity = 16)
|
||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
|
private var backupRefreshJob: Job? = null
|
||||||
|
private val backupActionMutex = Mutex()
|
||||||
|
|
||||||
private val dashboardData = combine(
|
private val dashboardData = combine(
|
||||||
readRecordRepository.getLatestReadRecords(),
|
homeDashboardUseCase.observe(),
|
||||||
readRecordRepository.getTotalReadTime(),
|
homeDashboardUseCase.observeSelectedSourceSetUrl(),
|
||||||
readRecordRepository.getAllRecordDetails(),
|
homeDashboardUseCase.observeVisibleSections(),
|
||||||
bookRepository.getAllBooks(),
|
) { dashboard, selectedSourceUrl, visibleSections ->
|
||||||
localPreferencesRepository.getPreference(
|
Triple(dashboard, selectedSourceUrl, visibleSections)
|
||||||
LocalPreferencesKeys.DAILY_READING_GOAL_MINUTES,
|
|
||||||
DEFAULT_DAILY_READING_GOAL_MINUTES,
|
|
||||||
),
|
|
||||||
) { records, totalReadTime, details, books, dailyGoalMinutes ->
|
|
||||||
val latestRecord = records.firstOrNull()
|
|
||||||
val latestBook = latestRecord?.let { record ->
|
|
||||||
books.firstOrNull {
|
|
||||||
it.name == record.bookName && it.author == record.bookAuthor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val today = LocalDate.now().toString()
|
|
||||||
|
|
||||||
HomeDashboardData(
|
|
||||||
totalReadBooks = records
|
|
||||||
.distinctBy { it.bookName to it.bookAuthor }
|
|
||||||
.size,
|
|
||||||
totalReadTimeMillis = totalReadTime,
|
|
||||||
todayReadTimeMillis = details
|
|
||||||
.asSequence()
|
|
||||||
.filter { it.date == today }
|
|
||||||
.sumOf { it.readTime },
|
|
||||||
dailyGoalMinutes = dailyGoalMinutes.coerceIn(
|
|
||||||
1,
|
|
||||||
MAX_DAILY_READING_GOAL_MINUTES,
|
|
||||||
),
|
|
||||||
recentBook = latestRecord?.let { record ->
|
|
||||||
HomeRecentBookUi(
|
|
||||||
bookUrl = latestBook?.bookUrl,
|
|
||||||
name = record.bookName,
|
|
||||||
author = record.bookAuthor,
|
|
||||||
origin = latestBook?.origin,
|
|
||||||
coverPath = latestBook?.getDisplayCover(),
|
|
||||||
chapterTitle = latestBook?.durChapterTitle,
|
|
||||||
chapterProgress = latestBook?.let { book ->
|
|
||||||
if (book.totalChapterNum > 0) {
|
|
||||||
(book.durChapterIndex + 1)
|
|
||||||
.coerceIn(0, book.totalChapterNum)
|
|
||||||
.toFloat() / book.totalChapterNum
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState = combine(
|
||||||
@@ -96,15 +57,24 @@ class HomeViewModel(
|
|||||||
_backupState,
|
_backupState,
|
||||||
_activeDialog,
|
_activeDialog,
|
||||||
_activeSheet,
|
_activeSheet,
|
||||||
) { dashboard, backup, dialog, sheet ->
|
) { dashboardData, backup, dialog, sheet ->
|
||||||
|
val (dashboard, selectedSourceUrl, visibleSections) = dashboardData
|
||||||
HomeUiState(
|
HomeUiState(
|
||||||
totalReadBooks = dashboard.totalReadBooks,
|
totalReadBooks = dashboard.totalReadBooks,
|
||||||
totalReadTimeMillis = dashboard.totalReadTimeMillis,
|
totalReadTimeMillis = dashboard.totalReadTimeMillis,
|
||||||
todayReadTimeMillis = dashboard.todayReadTimeMillis,
|
todayReadTimeMillis = dashboard.todayReadTimeMillis,
|
||||||
dailyGoalMinutes = dashboard.dailyGoalMinutes,
|
dailyGoalMinutes = dashboard.dailyGoalMinutes,
|
||||||
recentBook = dashboard.recentBook,
|
recentBook = dashboard.recentBooks.firstOrNull()?.toUi(),
|
||||||
|
recentBooks = dashboard.recentBooks
|
||||||
|
.drop(1)
|
||||||
|
.take(6)
|
||||||
|
.map { it.toUi() }
|
||||||
|
.toImmutableList(),
|
||||||
|
selectedSourceSetUrl = selectedSourceUrl,
|
||||||
|
visibleSections = visibleSections.toImmutableSet(),
|
||||||
latestBackup = backup.latest?.toUi(),
|
latestBackup = backup.latest?.toUi(),
|
||||||
isBackupLoading = backup.isLoading,
|
isBackupLoading = backup.isLoading,
|
||||||
|
isBackupLoadError = backup.isLoadError,
|
||||||
isBackupActionRunning = backup.isActionRunning,
|
isBackupActionRunning = backup.isActionRunning,
|
||||||
activeDialog = dialog,
|
activeDialog = dialog,
|
||||||
activeSheet = sheet,
|
activeSheet = sheet,
|
||||||
@@ -116,12 +86,33 @@ class HomeViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refreshLatestBackup()
|
viewModelScope.launch {
|
||||||
|
homeDashboardUseCase.observeVisibleSections()
|
||||||
|
.map { HomeDashboardSection.WebDavBackup in it }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { visible ->
|
||||||
|
if (visible) {
|
||||||
|
refreshLatestBackup()
|
||||||
|
} else {
|
||||||
|
backupRefreshJob?.cancel()
|
||||||
|
_backupState.update { it.copy(isLoading = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onIntent(intent: HomeIntent) {
|
fun onIntent(intent: HomeIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
HomeIntent.RecentBookClick -> openRecentBook()
|
HomeIntent.RecentBookClick -> openRecentBook()
|
||||||
|
is HomeIntent.RecentHistoryBookClick -> openBook(intent.bookUrl)
|
||||||
|
is HomeIntent.SelectSourceSet -> selectSourceSet(intent.sourceUrl)
|
||||||
|
HomeIntent.DashboardSettingsClick -> {
|
||||||
|
_activeSheet.value = HomeSheet.DashboardSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
is HomeIntent.SetSectionVisible -> {
|
||||||
|
setSectionVisible(intent.section, intent.visible)
|
||||||
|
}
|
||||||
HomeIntent.ReadingGoalClick -> {
|
HomeIntent.ReadingGoalClick -> {
|
||||||
_activeDialog.value = HomeDialog.SetReadingGoal(
|
_activeDialog.value = HomeDialog.SetReadingGoal(
|
||||||
uiState.value.dailyGoalMinutes
|
uiState.value.dailyGoalMinutes
|
||||||
@@ -159,6 +150,7 @@ class HomeViewModel(
|
|||||||
_effects.tryEmit(HomeEffect.OpenBackupSettings)
|
_effects.tryEmit(HomeEffect.OpenBackupSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HomeIntent.RetryBackupInfo -> refreshLatestBackup()
|
||||||
HomeIntent.DismissDialog -> _activeDialog.value = null
|
HomeIntent.DismissDialog -> _activeDialog.value = null
|
||||||
HomeIntent.DismissSheet -> _activeSheet.value = null
|
HomeIntent.DismissSheet -> _activeSheet.value = null
|
||||||
}
|
}
|
||||||
@@ -166,6 +158,10 @@ class HomeViewModel(
|
|||||||
|
|
||||||
private fun openRecentBook() {
|
private fun openRecentBook() {
|
||||||
val bookUrl = uiState.value.recentBook?.bookUrl ?: return
|
val bookUrl = uiState.value.recentBook?.bookUrl ?: return
|
||||||
|
openBook(bookUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBook(bookUrl: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bookRepository.getBook(bookUrl)?.let {
|
bookRepository.getBook(bookUrl)?.let {
|
||||||
_effects.emit(HomeEffect.OpenBook(it))
|
_effects.emit(HomeEffect.OpenBook(it))
|
||||||
@@ -174,20 +170,42 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateReadingGoal(minutes: Int) {
|
private fun updateReadingGoal(minutes: Int) {
|
||||||
val validMinutes = minutes.coerceIn(1, MAX_DAILY_READING_GOAL_MINUTES)
|
|
||||||
_activeDialog.value = null
|
_activeDialog.value = null
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
localPreferencesRepository.updatePreference(
|
homeDashboardUseCase.updateDailyGoal(minutes)
|
||||||
LocalPreferencesKeys.DAILY_READING_GOAL_MINUTES,
|
}
|
||||||
validMinutes,
|
}
|
||||||
)
|
|
||||||
|
private fun selectSourceSet(sourceUrl: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
homeDashboardUseCase.updateSelectedSourceSetUrl(sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSectionVisible(
|
||||||
|
section: HomeDashboardSection,
|
||||||
|
visible: Boolean,
|
||||||
|
) {
|
||||||
|
val sections = uiState.value.visibleSections.toMutableSet().apply {
|
||||||
|
if (visible) add(section) else remove(section)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
homeDashboardUseCase.updateVisibleSections(sections)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestRestore() {
|
private fun requestRestore() {
|
||||||
val backup = _backupState.value.latest
|
val backup = _backupState.value.latest
|
||||||
if (backup == null) {
|
if (backup == null) {
|
||||||
_effects.tryEmit(HomeEffect.ShowMessage(R.string.home_no_webdav_backup))
|
_effects.tryEmit(
|
||||||
|
HomeEffect.ShowMessage(
|
||||||
|
if (_backupState.value.isLoadError) {
|
||||||
|
R.string.home_webdav_backup_load_error
|
||||||
|
} else {
|
||||||
|
R.string.home_no_webdav_backup
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_activeDialog.value = HomeDialog.ConfirmRestore(backup.name)
|
_activeDialog.value = HomeDialog.ConfirmRestore(backup.name)
|
||||||
@@ -221,101 +239,130 @@ class HomeViewModel(
|
|||||||
path: String?,
|
path: String?,
|
||||||
savePath: Boolean = false,
|
savePath: Boolean = false,
|
||||||
) {
|
) {
|
||||||
if (_backupState.value.isActionRunning) return
|
if (!backupActionMutex.tryLock()) return
|
||||||
_backupState.update { it.copy(isActionRunning = true) }
|
_backupState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
runCatching {
|
try {
|
||||||
if (savePath) {
|
runCatching {
|
||||||
BackupConfig.backupPath = path
|
if (savePath) {
|
||||||
}
|
BackupConfig.backupPath = path
|
||||||
if (destination != HomeBackupDestination.Local) {
|
}
|
||||||
webDavBackupUseCase.refreshConfig()
|
if (destination != HomeBackupDestination.Local) {
|
||||||
}
|
webDavBackupUseCase.refreshConfig()
|
||||||
backupRestoreUseCase.backup(path, destination.mode)
|
}
|
||||||
}.onSuccess {
|
backupRestoreUseCase.backup(path, destination.mode)
|
||||||
_effects.emit(HomeEffect.ShowMessage(R.string.backup_success))
|
}.onSuccess {
|
||||||
if (destination != HomeBackupDestination.Local) {
|
_effects.emit(HomeEffect.ShowMessage(R.string.backup_success))
|
||||||
loadLatestBackup()
|
if (destination != HomeBackupDestination.Local) {
|
||||||
}
|
refreshLatestBackup()
|
||||||
}.onFailure { error ->
|
}
|
||||||
_effects.emit(
|
}.onFailure { error ->
|
||||||
HomeEffect.ShowMessage(
|
if (error is CancellationException) throw error
|
||||||
messageRes = R.string.backup_error,
|
_effects.emit(
|
||||||
detail = error.localizedMessage,
|
HomeEffect.ShowMessage(
|
||||||
|
messageRes = R.string.backup_error,
|
||||||
|
detail = error.localizedMessage,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
} finally {
|
||||||
|
_backupState.update { it.copy(isActionRunning = false) }
|
||||||
|
backupActionMutex.unlock()
|
||||||
}
|
}
|
||||||
_backupState.update { it.copy(isActionRunning = false) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreLocal(uri: String) {
|
private fun restoreLocal(uri: String) {
|
||||||
if (_backupState.value.isActionRunning) return
|
if (!backupActionMutex.tryLock()) return
|
||||||
_backupState.update { it.copy(isActionRunning = true) }
|
_backupState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
runCatching {
|
try {
|
||||||
backupRestoreUseCase.restoreLocal(uri)
|
runCatching {
|
||||||
}.onSuccess {
|
backupRestoreUseCase.restoreLocal(uri)
|
||||||
_effects.emit(HomeEffect.ShowMessage(R.string.restore_success))
|
}.onSuccess {
|
||||||
}.onFailure { error ->
|
_effects.emit(HomeEffect.ShowMessage(R.string.restore_success))
|
||||||
_effects.emit(
|
}.onFailure { error ->
|
||||||
HomeEffect.ShowMessage(
|
if (error is CancellationException) throw error
|
||||||
messageRes = R.string.restore_error,
|
_effects.emit(
|
||||||
detail = error.localizedMessage,
|
HomeEffect.ShowMessage(
|
||||||
|
messageRes = R.string.restore_error,
|
||||||
|
detail = error.localizedMessage,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
} finally {
|
||||||
|
_backupState.update { it.copy(isActionRunning = false) }
|
||||||
|
backupActionMutex.unlock()
|
||||||
}
|
}
|
||||||
_backupState.update { it.copy(isActionRunning = false) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restore() {
|
private fun restore() {
|
||||||
val backup = _backupState.value.latest ?: return
|
val backup = _backupState.value.latest ?: return
|
||||||
_activeDialog.value = null
|
_activeDialog.value = null
|
||||||
if (_backupState.value.isActionRunning) return
|
if (!backupActionMutex.tryLock()) return
|
||||||
_backupState.update { it.copy(isActionRunning = true) }
|
_backupState.update { it.copy(isActionRunning = true) }
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
runCatching {
|
try {
|
||||||
webDavBackupUseCase.restore(backup.name)
|
runCatching {
|
||||||
}.onSuccess {
|
webDavBackupUseCase.restore(backup.name)
|
||||||
_effects.emit(HomeEffect.ShowMessage(R.string.restore_success))
|
}.onSuccess {
|
||||||
}.onFailure { error ->
|
_effects.emit(HomeEffect.ShowMessage(R.string.restore_success))
|
||||||
_effects.emit(
|
}.onFailure { error ->
|
||||||
HomeEffect.ShowMessage(
|
if (error is CancellationException) throw error
|
||||||
messageRes = R.string.restore_error,
|
_effects.emit(
|
||||||
detail = error.localizedMessage,
|
HomeEffect.ShowMessage(
|
||||||
|
messageRes = R.string.restore_error,
|
||||||
|
detail = error.localizedMessage,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
} finally {
|
||||||
|
_backupState.update { it.copy(isActionRunning = false) }
|
||||||
|
backupActionMutex.unlock()
|
||||||
}
|
}
|
||||||
_backupState.update { it.copy(isActionRunning = false) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshLatestBackup() {
|
private fun refreshLatestBackup() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
backupRefreshJob?.cancel()
|
||||||
|
backupRefreshJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
loadLatestBackup()
|
loadLatestBackup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadLatestBackup() {
|
private suspend fun loadLatestBackup() {
|
||||||
_backupState.update { it.copy(isLoading = true) }
|
_backupState.update {
|
||||||
val latest = runCatching {
|
it.copy(
|
||||||
webDavBackupUseCase.getLatestBackup()
|
isLoading = true,
|
||||||
}.getOrNull()
|
isLoadError = false,
|
||||||
_backupState.update { it.copy(latest = latest, isLoading = false) }
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val latest = webDavBackupUseCase.getLatestBackup()
|
||||||
|
_backupState.update {
|
||||||
|
it.copy(
|
||||||
|
latest = latest,
|
||||||
|
isLoading = false,
|
||||||
|
isLoadError = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error: CancellationException) {
|
||||||
|
throw error
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_backupState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isLoadError = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class HomeDashboardData(
|
|
||||||
val totalReadBooks: Int,
|
|
||||||
val totalReadTimeMillis: Long,
|
|
||||||
val todayReadTimeMillis: Long,
|
|
||||||
val dailyGoalMinutes: Int,
|
|
||||||
val recentBook: HomeRecentBookUi?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class HomeBackupState(
|
private data class HomeBackupState(
|
||||||
val latest: WebDavBackup? = null,
|
val latest: WebDavBackup? = null,
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
val isLoadError: Boolean = false,
|
||||||
val isActionRunning: Boolean = false,
|
val isActionRunning: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,4 +370,14 @@ class HomeViewModel(
|
|||||||
name = name,
|
name = name,
|
||||||
lastModify = lastModify,
|
lastModify = lastModify,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun HomeReadingBook.toUi() = HomeRecentBookUi(
|
||||||
|
bookUrl = bookUrl,
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
origin = origin,
|
||||||
|
coverPath = coverPath,
|
||||||
|
chapterTitle = chapterTitle,
|
||||||
|
chapterProgress = chapterProgress,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package io.legado.app.ui.main.homepage
|
package io.legado.app.ui.main.homepage
|
||||||
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.ui.config.prefStateDelegate
|
import io.legado.app.ui.config.prefStateDelegate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,13 +8,6 @@ import io.legado.app.ui.config.prefStateDelegate
|
|||||||
*/
|
*/
|
||||||
object HomepageConfig {
|
object HomepageConfig {
|
||||||
|
|
||||||
/**
|
|
||||||
* 首页布局模式 0: 混合列表 1: 分源Tab
|
|
||||||
*/
|
|
||||||
private val _homepageLayoutMode = prefStateDelegate(PreferKey.homepageLayoutMode, 0)
|
|
||||||
var homepageLayoutMode by _homepageLayoutMode
|
|
||||||
val homepageLayoutModeState: State<Int> get() = _homepageLayoutMode.state
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页书源隐藏
|
* 首页书源隐藏
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ data class HomepageBookItemUi(
|
|||||||
data class HomepageUiState(
|
data class HomepageUiState(
|
||||||
val modules: ImmutableList<HomepageModuleUi> = persistentListOf(),
|
val modules: ImmutableList<HomepageModuleUi> = persistentListOf(),
|
||||||
val isManageMode: Boolean = false,
|
val isManageMode: Boolean = false,
|
||||||
val isConfigMode: Boolean = false,
|
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val manageState: HomepageManageUiState = HomepageManageUiState()
|
val manageState: HomepageManageUiState = HomepageManageUiState()
|
||||||
)
|
)
|
||||||
@@ -53,6 +52,16 @@ data class HomepageManageActions(
|
|||||||
val onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
|
val onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class HomepageFeedActions(
|
||||||
|
val onModuleHeaderClick: (String, String?, String?) -> Unit,
|
||||||
|
val onRetryModule: (String) -> Unit,
|
||||||
|
val onLoadMoreModule: (String) -> Unit,
|
||||||
|
val onBookClick: (SearchBook, String?) -> Unit,
|
||||||
|
val onKindUrlClick: (String, String, String) -> Unit,
|
||||||
|
val onRefreshButtonGroup: (String) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class HomepageSourceManageUi(
|
data class HomepageSourceManageUi(
|
||||||
val sourceUrl: String,
|
val sourceUrl: String,
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package io.legado.app.ui.main.homepage
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
||||||
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
|
||||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HomepageLayoutSheet(
|
|
||||||
show: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
layoutMode: Int,
|
|
||||||
onLayoutModeChange: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
HomepageLayoutSheet(
|
|
||||||
data = if (show) Unit else null,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
layoutMode = layoutMode,
|
|
||||||
onLayoutModeChange = onLayoutModeChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> HomepageLayoutSheet(
|
|
||||||
data: T?,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
layoutMode: Int,
|
|
||||||
onLayoutModeChange: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
AppModalBottomSheet(
|
|
||||||
data = data,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = stringResource(R.string.homepage_layout_settings),
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
CompactDropdownSettingItem(
|
|
||||||
title = stringResource(R.string.homepage_layout_mode),
|
|
||||||
selectedValue = layoutMode.toString(),
|
|
||||||
displayEntries = arrayOf(
|
|
||||||
stringResource(R.string.homepage_layout_mixed),
|
|
||||||
stringResource(R.string.homepage_layout_tabs)
|
|
||||||
),
|
|
||||||
entryValues = arrayOf("0", "1"),
|
|
||||||
onValueChange = { onLayoutModeChange(it.toInt()) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
package io.legado.app.ui.main.homepage
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.domain.model.HomepageModuleType
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||||
|
import io.legado.app.ui.main.homepage.modules.BannerModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.ButtonGroupModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.CardModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.GridModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.GridRankingModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.HomepageModuleSkeleton
|
||||||
|
import io.legado.app.ui.main.homepage.modules.RankingModule
|
||||||
|
import io.legado.app.ui.main.homepage.modules.WaterfallItem
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.LoadMoreFooter
|
||||||
|
import io.legado.app.ui.widget.components.book.SearchBookGridItem
|
||||||
|
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
|
||||||
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HomepageModuleFeed(
|
||||||
|
modules: List<HomepageModuleUi>,
|
||||||
|
actions: HomepageFeedActions,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
paddingValues: PaddingValues = PaddingValues(0.dp),
|
||||||
|
gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
|
||||||
|
verticalItemSpacing: Dp = 16.dp,
|
||||||
|
@StringRes emptyMessageRes: Int = R.string.homepage_add_module_definition,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
onBookLongClick: (SearchBook, String?) -> Unit = { _, _ -> },
|
||||||
|
onErrorClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
if (modules.isEmpty()) {
|
||||||
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
|
AppText(stringResource(emptyMessageRes))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val processedModules = remember(modules) {
|
||||||
|
fun isInfinite(m: HomepageModuleUi): Boolean {
|
||||||
|
return m.type == HomepageModuleType.Waterfall ||
|
||||||
|
m.type == HomepageModuleType.InfiniteGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
val infinite = modules.firstOrNull { isInfinite(it) }
|
||||||
|
val others = modules.filter { !isInfinite(it) }
|
||||||
|
if (infinite != null) others + infinite else others
|
||||||
|
}
|
||||||
|
|
||||||
|
val gridColumns = remember(processedModules) {
|
||||||
|
val infiniteModule = processedModules.find { m ->
|
||||||
|
m.type == HomepageModuleType.Waterfall ||
|
||||||
|
m.type == HomepageModuleType.InfiniteGrid
|
||||||
|
}
|
||||||
|
infiniteModule?.config?.get("layout_columns")?.toIntOrNull() ?: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Fixed(gridColumns),
|
||||||
|
state = gridState,
|
||||||
|
modifier = modifier,
|
||||||
|
verticalItemSpacing = verticalItemSpacing,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) {
|
||||||
|
120.dp
|
||||||
|
} else {
|
||||||
|
8.dp
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
processedModules.forEach { moduleUi ->
|
||||||
|
item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) {
|
||||||
|
ModuleHeader(
|
||||||
|
title = moduleUi.title,
|
||||||
|
onNavigate = if (moduleUi.type == HomepageModuleType.ButtonGroup) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
actions.onModuleHeaderClick(
|
||||||
|
moduleUi.sourceUrl,
|
||||||
|
moduleUi.exploreUrl,
|
||||||
|
moduleUi.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val state = moduleUi.state) {
|
||||||
|
is ModuleLoadState.Loading,
|
||||||
|
is ModuleLoadState.Error -> {
|
||||||
|
item(
|
||||||
|
key = "status_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = state,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
|
||||||
|
},
|
||||||
|
contentKey = { it::class },
|
||||||
|
) { targetState ->
|
||||||
|
when (targetState) {
|
||||||
|
is ModuleLoadState.Loading -> {
|
||||||
|
HomepageModuleSkeleton(
|
||||||
|
type = moduleUi.type,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModuleLoadState.Error -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
GlassCard(
|
||||||
|
onClick = { onErrorClick(targetState.message) },
|
||||||
|
containerColor = LegadoTheme.colorScheme.errorContainer.copy(
|
||||||
|
alpha = 0.6f
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 16.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
12.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Outlined.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
|
||||||
|
AppText(
|
||||||
|
text = targetState.message,
|
||||||
|
color = LegadoTheme.colorScheme.error,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
color = LegadoTheme.colorScheme.error.copy(
|
||||||
|
alpha = 0.3f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
actions.onRetryModule(moduleUi.globalId)
|
||||||
|
}
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
8.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Default.Refresh,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.retry),
|
||||||
|
color = LegadoTheme.colorScheme.error,
|
||||||
|
style = LegadoTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModuleLoadState.Buttons -> {
|
||||||
|
item(
|
||||||
|
key = "buttons_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
ButtonGroupModule(
|
||||||
|
kinds = state.kinds,
|
||||||
|
sourceUrl = moduleUi.sourceUrl,
|
||||||
|
globalId = moduleUi.globalId,
|
||||||
|
onOpenKind = actions.onKindUrlClick,
|
||||||
|
onRefreshKinds = actions.onRefreshButtonGroup,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
layoutConfig = moduleUi.layoutConfig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModuleLoadState.Loaded -> {
|
||||||
|
val config = moduleUi.config
|
||||||
|
when (moduleUi.type) {
|
||||||
|
HomepageModuleType.Waterfall -> {
|
||||||
|
itemsIndexed(
|
||||||
|
state.books,
|
||||||
|
key = { index, item ->
|
||||||
|
"wf_${moduleUi.globalId}_${item.book.bookUrl}_$index"
|
||||||
|
},
|
||||||
|
) { index, item ->
|
||||||
|
val sharedCoverKey = bookCoverSharedElementKey(
|
||||||
|
item.book.bookUrl,
|
||||||
|
"home:${moduleUi.globalId}:waterfall:$index"
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
WaterfallItem(
|
||||||
|
item = item,
|
||||||
|
onClick = {
|
||||||
|
actions.onBookClick(
|
||||||
|
item.book,
|
||||||
|
sharedCoverKey
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(
|
||||||
|
key = "wf_more_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
LoadMoreFooter(
|
||||||
|
isLoading = state.isLoadingMore,
|
||||||
|
errorMsg = null,
|
||||||
|
isEnd = !state.hasMore,
|
||||||
|
onRetry = { actions.onLoadMoreModule(moduleUi.globalId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.InfiniteGrid -> {
|
||||||
|
itemsIndexed(
|
||||||
|
state.books,
|
||||||
|
key = { index, item ->
|
||||||
|
"inf_grid_${moduleUi.globalId}_${item.book.bookUrl}_$index"
|
||||||
|
},
|
||||||
|
) { index, item ->
|
||||||
|
val sharedCoverKey = bookCoverSharedElementKey(
|
||||||
|
item.book.bookUrl,
|
||||||
|
"home:${moduleUi.globalId}:infinite:$index"
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
SearchBookGridItem(
|
||||||
|
book = item.book,
|
||||||
|
shelfState = item.shelfState,
|
||||||
|
onClick = {
|
||||||
|
actions.onBookClick(
|
||||||
|
item.book,
|
||||||
|
sharedCoverKey
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKey = sharedCoverKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(
|
||||||
|
key = "inf_grid_more_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
LoadMoreFooter(
|
||||||
|
isLoading = state.isLoadingMore,
|
||||||
|
errorMsg = null,
|
||||||
|
isEnd = !state.hasMore,
|
||||||
|
onRetry = { actions.onLoadMoreModule(moduleUi.globalId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.Grid -> {
|
||||||
|
val rows = config["layout_rows"]?.toIntOrNull() ?: 2
|
||||||
|
val columns = config["layout_columns"]?.toIntOrNull() ?: 3
|
||||||
|
item(
|
||||||
|
key = "content_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
GridModule(
|
||||||
|
books = state.books,
|
||||||
|
onClick = { book, sharedCoverKey ->
|
||||||
|
actions.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
columns = columns,
|
||||||
|
maxRows = rows,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.Banner -> {
|
||||||
|
item(
|
||||||
|
key = "content_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
BannerModule(
|
||||||
|
books = state.books,
|
||||||
|
onClick = { book, sharedCoverKey ->
|
||||||
|
actions.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.Ranking -> {
|
||||||
|
item(
|
||||||
|
key = "content_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
RankingModule(
|
||||||
|
books = state.books,
|
||||||
|
onClick = { book, sharedCoverKey ->
|
||||||
|
actions.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.GridRanking -> {
|
||||||
|
item(
|
||||||
|
key = "content_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
GridRankingModule(
|
||||||
|
books = state.books,
|
||||||
|
onClick = { book, sharedCoverKey ->
|
||||||
|
actions.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomepageModuleType.Card -> {
|
||||||
|
item(
|
||||||
|
key = "content_${moduleUi.globalId}",
|
||||||
|
span = StaggeredGridItemSpan.FullLine
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
|
CardModule(
|
||||||
|
books = state.books,
|
||||||
|
onClick = { book, sharedCoverKey ->
|
||||||
|
actions.onBookClick(book, sharedCoverKey)
|
||||||
|
},
|
||||||
|
onLongClick = onBookLongClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModuleHeader(
|
||||||
|
title: String,
|
||||||
|
onNavigate: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (onNavigate != null) {
|
||||||
|
SmallTonalButton(
|
||||||
|
onClick = onNavigate,
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowForward
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,795 +0,0 @@
|
|||||||
package io.legado.app.ui.main.homepage
|
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
|
||||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
|
||||||
import androidx.compose.material.icons.filled.GridView
|
|
||||||
import androidx.compose.material.icons.filled.Explore
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material.icons.outlined.Info
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
|
||||||
import io.legado.app.domain.model.HomepageModuleType
|
|
||||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
|
||||||
import io.legado.app.ui.main.homepage.modules.BannerModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.ButtonGroupModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.CardModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.GridModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.GridRankingModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.HomepageModuleSkeleton
|
|
||||||
import io.legado.app.ui.main.homepage.modules.RankingModule
|
|
||||||
import io.legado.app.ui.main.homepage.modules.WaterfallItem
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
|
||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
|
||||||
import io.legado.app.ui.theme.adaptiveContentPaddingBookshelf
|
|
||||||
import io.legado.app.ui.widget.components.AppPullToRefresh
|
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
|
||||||
import io.legado.app.ui.widget.components.LoadMoreFooter
|
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
|
||||||
import io.legado.app.ui.widget.components.book.SearchBookGridItem
|
|
||||||
import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
|
|
||||||
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
|
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
|
||||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
|
||||||
import io.legado.app.ui.widget.components.EmptyMessage
|
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
|
||||||
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
|
||||||
import io.legado.app.utils.sendToClip
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
|
|
||||||
@OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalMaterial3ExpressiveApi::class,
|
|
||||||
ExperimentalSharedTransitionApi::class
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun HomepageScreen(
|
|
||||||
viewModel: HomepageViewModel = koinViewModel(),
|
|
||||||
onBookClick: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit,
|
|
||||||
onModuleHeaderClick: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
|
||||||
onSwitchToDiscovery: () -> Unit,
|
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
||||||
var previewBook by remember { mutableStateOf<SearchBook?>(null) }
|
|
||||||
var previewSharedCoverKey by remember { mutableStateOf<String?>(null) }
|
|
||||||
// Removed allSets and browseSources as they are now part of uiState.manageState
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalContext.current
|
|
||||||
var errorMsg by remember { mutableStateOf<String?>(null) }
|
|
||||||
var showPageMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val layoutMode = HomepageConfig.homepageLayoutModeState.value
|
|
||||||
|
|
||||||
val selectedSets = remember(uiState.manageState.sets) {
|
|
||||||
uiState.manageState.sets.filter { it.isSelected }
|
|
||||||
}
|
|
||||||
val pagerState = rememberPagerState(pageCount = {
|
|
||||||
selectedSets.size.coerceAtLeast(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
val discoveryString = stringResource(R.string.discovery)
|
|
||||||
val currentTitle by remember(layoutMode, selectedSets) {
|
|
||||||
derivedStateOf {
|
|
||||||
if (layoutMode == 1) {
|
|
||||||
discoveryString
|
|
||||||
} else {
|
|
||||||
selectedSets.getOrNull(pagerState.currentPage)?.sourceName ?: discoveryString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler(enabled = uiState.isManageMode || uiState.isConfigMode) {
|
|
||||||
if (uiState.isManageMode) viewModel.toggleManageMode()
|
|
||||||
else viewModel.toggleConfigMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(viewModel) {
|
|
||||||
viewModel.effects.collectLatest { effect ->
|
|
||||||
when (effect) {
|
|
||||||
is HomepageEffect.NavigateToBookInfo ->
|
|
||||||
onBookClick(
|
|
||||||
effect.name,
|
|
||||||
effect.author,
|
|
||||||
effect.bookUrl,
|
|
||||||
effect.origin,
|
|
||||||
effect.coverPath,
|
|
||||||
effect.sharedCoverKey
|
|
||||||
)
|
|
||||||
|
|
||||||
is HomepageEffect.NavigateToExploreShow ->
|
|
||||||
onModuleHeaderClick(effect.title, effect.sourceUrl, effect.exploreUrl)
|
|
||||||
|
|
||||||
is HomepageEffect.ShowSnackbar -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
|
||||||
AppScaffold(
|
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
contentWindowInsets = WindowInsets(0),
|
|
||||||
topBar = {
|
|
||||||
GlassMediumFlexibleTopAppBar(
|
|
||||||
title = currentTitle,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
actions = {
|
|
||||||
TopBarActionButton(
|
|
||||||
onClick = { viewModel.toggleManageMode() },
|
|
||||||
imageVector = AppIcons.Settings,
|
|
||||||
contentDescription = "Manage Modules",
|
|
||||||
)
|
|
||||||
Box {
|
|
||||||
TopBarActionButton(
|
|
||||||
onClick = { showPageMenu = true },
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "更多",
|
|
||||||
)
|
|
||||||
RoundDropdownMenu(
|
|
||||||
expanded = showPageMenu,
|
|
||||||
onDismissRequest = { showPageMenu = false },
|
|
||||||
) {
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
MenuItemIcon(Icons.Default.GridView)
|
|
||||||
},
|
|
||||||
text = stringResource(R.string.homepage_layout_settings),
|
|
||||||
onClick = {
|
|
||||||
showPageMenu = false
|
|
||||||
viewModel.toggleConfigMode()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
MenuItemIcon(Icons.Default.Explore)
|
|
||||||
},
|
|
||||||
text = stringResource(R.string.switch_to_classic_discovery),
|
|
||||||
onClick = {
|
|
||||||
showPageMenu = false
|
|
||||||
onSwitchToDiscovery()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bottomContent = {
|
|
||||||
if (layoutMode == 1 && selectedSets.isNotEmpty()) {
|
|
||||||
AppTabRow(
|
|
||||||
tabTitles = selectedSets.map { it.sourceName },
|
|
||||||
selectedTabIndex = pagerState.currentPage.coerceIn(
|
|
||||||
0,
|
|
||||||
selectedSets.size - 1
|
|
||||||
),
|
|
||||||
onTabSelected = { index ->
|
|
||||||
scope.launch { pagerState.animateScrollToPage(index) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { paddingValues ->
|
|
||||||
AppPullToRefresh(
|
|
||||||
isRefreshing = uiState.isRefreshing,
|
|
||||||
onRefresh = { viewModel.onRefresh() },
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
if (selectedSets.isEmpty()) {
|
|
||||||
EmptyMessage(
|
|
||||||
messageResId = R.string.homepage_no_source_sets_selected,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
HorizontalPager(
|
|
||||||
state = pagerState,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
key = { index -> selectedSets.getOrNull(index)?.sourceUrl ?: index }
|
|
||||||
) { pageIndex ->
|
|
||||||
val source = selectedSets.getOrNull(pageIndex)
|
|
||||||
val sourceModules = remember(uiState.modules, source) {
|
|
||||||
uiState.modules.filter { module ->
|
|
||||||
if (source?.isCustomSet == true) {
|
|
||||||
val setId =
|
|
||||||
HomepageViewModel.customSetIdFromUrl(source.sourceUrl)
|
|
||||||
module.customSetId == setId
|
|
||||||
} else {
|
|
||||||
module.sourceUrl == source?.sourceUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ModuleList(
|
|
||||||
modules = sourceModules,
|
|
||||||
viewModel = viewModel,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
paddingValues = paddingValues,
|
|
||||||
onErrorClick = { errorMsg = it },
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
onBookLongClick = { book, sharedCoverKey ->
|
|
||||||
previewBook = book
|
|
||||||
previewSharedCoverKey = sharedCoverKey
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppAlertDialog(
|
|
||||||
data = errorMsg,
|
|
||||||
onDismissRequest = { errorMsg = null },
|
|
||||||
title = stringResource(R.string.homepage_module_error),
|
|
||||||
confirmText = stringResource(R.string.copy_text),
|
|
||||||
onConfirm = {
|
|
||||||
context.sendToClip(it)
|
|
||||||
errorMsg = null
|
|
||||||
},
|
|
||||||
dismissText = stringResource(R.string.close),
|
|
||||||
onDismiss = { errorMsg = null },
|
|
||||||
content = { msg ->
|
|
||||||
SelectionContainer {
|
|
||||||
AppText(
|
|
||||||
text = msg,
|
|
||||||
style = LegadoTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.heightIn(max = 400.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
HomepageModuleManageSheet(
|
|
||||||
data = if (uiState.isManageMode) Unit else null,
|
|
||||||
onDismissRequest = { viewModel.toggleManageMode() },
|
|
||||||
state = uiState.manageState,
|
|
||||||
actions = remember {
|
|
||||||
HomepageManageActions(
|
|
||||||
onToggleSet = { url, isEnabled ->
|
|
||||||
viewModel.toggleSourceFilter(
|
|
||||||
url,
|
|
||||||
isEnabled
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
|
|
||||||
onSyncSourceModules = { viewModel.syncSourceModules(it) },
|
|
||||||
onToggleModule = { id, visible -> viewModel.setModuleVisible(id, visible) },
|
|
||||||
onJoinModule = { sourceUrl, targetSetId, def ->
|
|
||||||
viewModel.joinModule(sourceUrl, targetSetId, def)
|
|
||||||
},
|
|
||||||
onAddCustomModule = { sourceUrl, targetSetId, def ->
|
|
||||||
viewModel.addCustomModule(sourceUrl, targetSetId, def)
|
|
||||||
},
|
|
||||||
onAddButtonGroupFromKinds = { sourceUrl, targetSetId, title, kinds ->
|
|
||||||
viewModel.addButtonGroupFromKinds(sourceUrl, targetSetId, title, kinds)
|
|
||||||
},
|
|
||||||
onGetExploreKinds = { viewModel.getSourceExploreKinds(it) },
|
|
||||||
onUpdateModule = { globalId, def -> viewModel.updateModule(globalId, def) },
|
|
||||||
onDeleteModule = { viewModel.deleteModule(it) },
|
|
||||||
onReorderModules = { ids -> viewModel.reorderJoinedModules(ids) },
|
|
||||||
onReorderSets = { urls -> viewModel.reorderCustomSets(urls) },
|
|
||||||
onSetCustomSetTitle = { id, title ->
|
|
||||||
viewModel.setModuleCustomSetTitle(
|
|
||||||
id,
|
|
||||||
title
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onCreateCustomSet = { viewModel.createCustomSet(it) },
|
|
||||||
onRenameCustomSet = { id, name -> viewModel.renameCustomSet(id, name) },
|
|
||||||
onDeleteCustomSet = { viewModel.deleteCustomSet(it) },
|
|
||||||
onAssignModuleToCustomSet = { id, setId ->
|
|
||||||
viewModel.assignModuleToCustomSet(id, setId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
HomepageLayoutSheet(
|
|
||||||
data = if (uiState.isConfigMode) Unit else null,
|
|
||||||
onDismissRequest = { viewModel.toggleConfigMode() },
|
|
||||||
layoutMode = layoutMode,
|
|
||||||
onLayoutModeChange = { viewModel.setLayoutMode(it) },
|
|
||||||
)
|
|
||||||
|
|
||||||
SearchBookPreviewSheet(
|
|
||||||
data = previewBook,
|
|
||||||
shelfState = previewBook?.let { viewModel.getCurrentBookShelfState(it) },
|
|
||||||
sharedCoverKey = previewSharedCoverKey,
|
|
||||||
onDismissRequest = { previewBook = null },
|
|
||||||
onOpenDetail = { book, sharedCoverKey ->
|
|
||||||
previewBook = null
|
|
||||||
onBookClick(
|
|
||||||
book.name, book.author, book.bookUrl,
|
|
||||||
book.origin, book.coverUrl, sharedCoverKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onAddToShelf = { book ->
|
|
||||||
viewModel.onAddToShelf(book)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun ModuleList(
|
|
||||||
modules: List<HomepageModuleUi>,
|
|
||||||
viewModel: HomepageViewModel,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
paddingValues: PaddingValues = PaddingValues(0.dp),
|
|
||||||
gridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
|
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
|
||||||
onBookLongClick: (SearchBook, String?) -> Unit = { _, _ -> },
|
|
||||||
onErrorClick: (String) -> Unit
|
|
||||||
) {
|
|
||||||
if (modules.isEmpty()) {
|
|
||||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
|
||||||
AppText(stringResource(R.string.homepage_add_module_definition))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 1. 过滤和重排模块:每个集只能有一个无限流模块,且必须在最下面
|
|
||||||
val processedModules = remember(modules) {
|
|
||||||
fun isInfinite(m: HomepageModuleUi): Boolean {
|
|
||||||
return m.type == HomepageModuleType.Waterfall ||
|
|
||||||
m.type == HomepageModuleType.InfiniteGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
val infinite = modules.firstOrNull { isInfinite(it) }
|
|
||||||
val others = modules.filter { !isInfinite(it) }
|
|
||||||
if (infinite != null) others + infinite else others
|
|
||||||
}
|
|
||||||
|
|
||||||
val gridColumns = remember(processedModules) {
|
|
||||||
val infiniteModule = processedModules.find { m ->
|
|
||||||
m.type == HomepageModuleType.Waterfall ||
|
|
||||||
m.type == HomepageModuleType.InfiniteGrid
|
|
||||||
}
|
|
||||||
infiniteModule?.config?.get("layout_columns")?.toIntOrNull() ?: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyVerticalStaggeredGrid(
|
|
||||||
columns = StaggeredGridCells.Fixed(gridColumns),
|
|
||||||
state = gridState,
|
|
||||||
modifier = modifier,
|
|
||||||
verticalItemSpacing = 16.dp,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
contentPadding = adaptiveContentPadding(
|
|
||||||
top = paddingValues.calculateTopPadding(),
|
|
||||||
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
processedModules.forEach { moduleUi ->
|
|
||||||
// 1. 头部 (全宽)
|
|
||||||
item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) {
|
|
||||||
ModuleHeader(
|
|
||||||
title = moduleUi.title,
|
|
||||||
onNavigate = if (moduleUi.type == HomepageModuleType.ButtonGroup) null else {
|
|
||||||
{
|
|
||||||
viewModel.onModuleHeaderClick(
|
|
||||||
moduleUi.sourceUrl,
|
|
||||||
moduleUi.exploreUrl,
|
|
||||||
moduleUi.title,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 内容正文
|
|
||||||
when (val state = moduleUi.state) {
|
|
||||||
is ModuleLoadState.Loading,
|
|
||||||
is ModuleLoadState.Error -> {
|
|
||||||
item(
|
|
||||||
key = "status_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = state,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
|
|
||||||
},
|
|
||||||
contentKey = { it::class },
|
|
||||||
) { targetState ->
|
|
||||||
when (targetState) {
|
|
||||||
is ModuleLoadState.Loading -> {
|
|
||||||
HomepageModuleSkeleton(
|
|
||||||
type = moduleUi.type,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ModuleLoadState.Error -> {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
GlassCard(
|
|
||||||
onClick = { onErrorClick(targetState.message) },
|
|
||||||
containerColor = LegadoTheme.colorScheme.errorContainer.copy(
|
|
||||||
alpha = 0.6f
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 16.dp
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
AppIcon(
|
|
||||||
imageVector = Icons.Outlined.Info,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = LegadoTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
|
|
||||||
AppText(
|
|
||||||
text = targetState.message,
|
|
||||||
color = LegadoTheme.colorScheme.error,
|
|
||||||
style = LegadoTheme.typography.bodySmall,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
viewModel.retryModule(moduleUi.globalId)
|
|
||||||
}
|
|
||||||
.padding(vertical = 10.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
AppIcon(
|
|
||||||
imageVector = Icons.Default.Refresh,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = LegadoTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
|
|
||||||
AppText(
|
|
||||||
text = stringResource(R.string.retry),
|
|
||||||
color = LegadoTheme.colorScheme.error,
|
|
||||||
style = LegadoTheme.typography.labelMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ModuleLoadState.Buttons -> {
|
|
||||||
item(
|
|
||||||
key = "buttons_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
ButtonGroupModule(
|
|
||||||
kinds = state.kinds,
|
|
||||||
sourceUrl = moduleUi.sourceUrl,
|
|
||||||
globalId = moduleUi.globalId,
|
|
||||||
viewModel = viewModel,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
layoutConfig = moduleUi.layoutConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ModuleLoadState.Loaded -> {
|
|
||||||
val config = moduleUi.config
|
|
||||||
when (moduleUi.type) {
|
|
||||||
HomepageModuleType.Waterfall -> {
|
|
||||||
itemsIndexed(
|
|
||||||
state.books,
|
|
||||||
key = { index, item -> "wf_${moduleUi.globalId}_${item.book.bookUrl}_$index" }) { index, item ->
|
|
||||||
val sharedCoverKey = bookCoverSharedElementKey(
|
|
||||||
item.book.bookUrl,
|
|
||||||
"home:${moduleUi.globalId}:waterfall:$index"
|
|
||||||
)
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
WaterfallItem(
|
|
||||||
item = item,
|
|
||||||
onClick = {
|
|
||||||
viewModel.onBookClick(
|
|
||||||
item.book,
|
|
||||||
sharedCoverKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKey = sharedCoverKey,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item(
|
|
||||||
key = "wf_more_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
LoadMoreFooter(
|
|
||||||
isLoading = state.isLoadingMore,
|
|
||||||
errorMsg = null,
|
|
||||||
isEnd = !state.hasMore,
|
|
||||||
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomepageModuleType.InfiniteGrid -> {
|
|
||||||
itemsIndexed(
|
|
||||||
state.books,
|
|
||||||
key = { index, item -> "inf_grid_${moduleUi.globalId}_${item.book.bookUrl}_$index" }) { index, item ->
|
|
||||||
val sharedCoverKey = bookCoverSharedElementKey(
|
|
||||||
item.book.bookUrl,
|
|
||||||
"home:${moduleUi.globalId}:infinite:$index"
|
|
||||||
)
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
SearchBookGridItem(
|
|
||||||
book = item.book,
|
|
||||||
shelfState = item.shelfState,
|
|
||||||
onClick = {
|
|
||||||
viewModel.onBookClick(
|
|
||||||
item.book,
|
|
||||||
sharedCoverKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKey = sharedCoverKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item(
|
|
||||||
key = "inf_grid_more_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
LoadMoreFooter(
|
|
||||||
isLoading = state.isLoadingMore,
|
|
||||||
errorMsg = null,
|
|
||||||
isEnd = !state.hasMore,
|
|
||||||
onRetry = { viewModel.loadMoreModule(moduleUi.globalId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomepageModuleType.Grid -> {
|
|
||||||
val rows = config["layout_rows"]?.toIntOrNull() ?: 2
|
|
||||||
val columns = config["layout_columns"]?.toIntOrNull() ?: 3
|
|
||||||
item(
|
|
||||||
key = "content_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
GridModule(
|
|
||||||
books = state.books,
|
|
||||||
onClick = { book, sharedCoverKey ->
|
|
||||||
viewModel.onBookClick(book, sharedCoverKey)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
columns = columns,
|
|
||||||
maxRows = rows,
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
when (moduleUi.type) {
|
|
||||||
HomepageModuleType.Banner -> {
|
|
||||||
item(
|
|
||||||
key = "content_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
BannerModule(
|
|
||||||
books = state.books,
|
|
||||||
onClick = { book, sharedCoverKey ->
|
|
||||||
viewModel.onBookClick(book, sharedCoverKey)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomepageModuleType.Ranking -> {
|
|
||||||
item(
|
|
||||||
key = "content_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
RankingModule(
|
|
||||||
books = state.books,
|
|
||||||
onClick = { book, sharedCoverKey ->
|
|
||||||
viewModel.onBookClick(book, sharedCoverKey)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomepageModuleType.GridRanking -> {
|
|
||||||
item(
|
|
||||||
key = "content_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
GridRankingModule(
|
|
||||||
books = state.books,
|
|
||||||
onClick = { book, sharedCoverKey ->
|
|
||||||
viewModel.onBookClick(book, sharedCoverKey)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomepageModuleType.Card -> {
|
|
||||||
item(
|
|
||||||
key = "content_${moduleUi.globalId}",
|
|
||||||
span = StaggeredGridItemSpan.FullLine
|
|
||||||
) {
|
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
|
||||||
CardModule(
|
|
||||||
books = state.books,
|
|
||||||
onClick = { book, sharedCoverKey ->
|
|
||||||
viewModel.onBookClick(book, sharedCoverKey)
|
|
||||||
},
|
|
||||||
onLongClick = onBookLongClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
|
||||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ModuleHeader(
|
|
||||||
title: String,
|
|
||||||
onNavigate: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp, bottom = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
AppText(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
if (onNavigate != null) {
|
|
||||||
SmallTonalButton(
|
|
||||||
onClick = onNavigate,
|
|
||||||
icon = Icons.AutoMirrored.Filled.ArrowForward
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package io.legado.app.ui.main.homepage
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
|
||||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HomepageSourceSelectSheet(
|
|
||||||
show: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
sources: List<HomepageSourceManageUi>,
|
|
||||||
onToggleSource: (String) -> Unit,
|
|
||||||
onSelectAll: () -> Unit,
|
|
||||||
) {
|
|
||||||
var filterText by remember(show) { mutableStateOf("") }
|
|
||||||
|
|
||||||
val filteredSources = remember(sources, filterText) {
|
|
||||||
if (filterText.isBlank()) sources else sources.filter {
|
|
||||||
it.sourceName.contains(filterText, ignoreCase = true) ||
|
|
||||||
it.sourceGroup?.contains(filterText, ignoreCase = true) == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isAllSelected = remember(sources) {
|
|
||||||
sources.all { it.isSelected } || sources.none { it.isSelected }
|
|
||||||
}
|
|
||||||
|
|
||||||
AppModalBottomSheet(
|
|
||||||
show = show,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = stringResource(R.string.homepage_filter_sources),
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
SearchBar(
|
|
||||||
query = filterText,
|
|
||||||
onQueryChange = { filterText = it },
|
|
||||||
placeholder = stringResource(R.string.screen),
|
|
||||||
autoFocus = false
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
SelectionItemCard(
|
|
||||||
title = stringResource(R.string.all_source),
|
|
||||||
isSelected = isAllSelected,
|
|
||||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
|
||||||
inSelectionMode = true,
|
|
||||||
onToggleSelection = {
|
|
||||||
onSelectAll()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.heightIn(max = 480.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
items(filteredSources, key = { it.sourceUrl }) { source ->
|
|
||||||
SelectionItemCard(
|
|
||||||
title = source.sourceName,
|
|
||||||
subtitle = source.sourceGroup?.takeIf { it.isNotBlank() },
|
|
||||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
|
||||||
isSelected = source.isSelected,
|
|
||||||
inSelectionMode = true,
|
|
||||||
onToggleSelection = {
|
|
||||||
onToggleSource(source.sourceUrl)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -97,7 +97,6 @@ class HomepageViewModel(
|
|||||||
// 1. 基础原始状态
|
// 1. 基础原始状态
|
||||||
private val _isRefreshing = MutableStateFlow(false)
|
private val _isRefreshing = MutableStateFlow(false)
|
||||||
private val _isManageMode = MutableStateFlow(false)
|
private val _isManageMode = MutableStateFlow(false)
|
||||||
private val _isConfigMode = MutableStateFlow(false)
|
|
||||||
private val _configVersion = MutableStateFlow(0L)
|
private val _configVersion = MutableStateFlow(0L)
|
||||||
private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
|
private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
|
||||||
private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
|
private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
|
||||||
@@ -151,8 +150,8 @@ class HomepageViewModel(
|
|||||||
|
|
||||||
// 4. 聚合层
|
// 4. 聚合层
|
||||||
private val uiFlagsFlow =
|
private val uiFlagsFlow =
|
||||||
combine(_isRefreshing, _isManageMode, _isConfigMode) { refreshing, manage, config ->
|
combine(_isRefreshing, _isManageMode) { refreshing, manage ->
|
||||||
HomepageUiFlags(refreshing, manage, config)
|
HomepageUiFlags(refreshing, manage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val manageStateFlow = combine(
|
private val manageStateFlow = combine(
|
||||||
@@ -275,7 +274,6 @@ class HomepageViewModel(
|
|||||||
modules = modules,
|
modules = modules,
|
||||||
isRefreshing = flags.isRefreshing,
|
isRefreshing = flags.isRefreshing,
|
||||||
isManageMode = flags.isManageMode,
|
isManageMode = flags.isManageMode,
|
||||||
isConfigMode = flags.isConfigMode,
|
|
||||||
manageState = manageState
|
manageState = manageState
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
|
||||||
@@ -550,7 +548,6 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toggleManageMode() = _isManageMode.update { !it }
|
fun toggleManageMode() = _isManageMode.update { !it }
|
||||||
fun toggleConfigMode() = _isConfigMode.update { !it }
|
|
||||||
|
|
||||||
fun setModuleVisible(id: String, visible: Boolean) {
|
fun setModuleVisible(id: String, visible: Boolean) {
|
||||||
_pendingEnabled.update { it + (id to visible) }
|
_pendingEnabled.update { it + (id to visible) }
|
||||||
@@ -579,11 +576,6 @@ class HomepageViewModel(
|
|||||||
notifyConfigChanged()
|
notifyConfigChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLayoutMode(mode: Int) {
|
|
||||||
HomepageConfig.homepageLayoutMode = mode
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
|
private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
|
||||||
val setId = "src_$sourceUrl"
|
val setId = "src_$sourceUrl"
|
||||||
if (gateway.getCustomSetById(setId) == null) gateway.upsertCustomSet(
|
if (gateway.getCustomSetById(setId) == null) gateway.upsertCustomSet(
|
||||||
@@ -875,6 +867,12 @@ class HomepageViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveSearchBook(book: SearchBook) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
saveSearchBooksUseCase.save(book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onBookClick(book: SearchBook, sharedCoverKey: String?) {
|
fun onBookClick(book: SearchBook, sharedCoverKey: String?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveSearchBooksUseCase.save(book)
|
saveSearchBooksUseCase.save(book)
|
||||||
@@ -940,6 +938,5 @@ class HomepageViewModel(
|
|||||||
|
|
||||||
private data class HomepageUiFlags(
|
private data class HomepageUiFlags(
|
||||||
val isRefreshing: Boolean,
|
val isRefreshing: Boolean,
|
||||||
val isManageMode: Boolean,
|
val isManageMode: Boolean
|
||||||
val isConfigMode: Boolean
|
|
||||||
)
|
)
|
||||||
@@ -22,7 +22,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||||
import io.legado.app.ui.main.homepage.HomepageViewModel
|
|
||||||
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.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
@@ -37,7 +36,8 @@ fun ButtonGroupModule(
|
|||||||
kinds: List<ExploreKind>,
|
kinds: List<ExploreKind>,
|
||||||
sourceUrl: String,
|
sourceUrl: String,
|
||||||
globalId: String,
|
globalId: String,
|
||||||
viewModel: HomepageViewModel,
|
onOpenKind: (sourceUrl: String, url: String, title: String) -> Unit,
|
||||||
|
onRefreshKinds: (globalId: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
icon: String? = null,
|
icon: String? = null,
|
||||||
layoutConfig: String? = null,
|
layoutConfig: String? = null,
|
||||||
@@ -91,10 +91,10 @@ fun ButtonGroupModule(
|
|||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
activity = activity,
|
activity = activity,
|
||||||
onOpenUrl = { url ->
|
onOpenUrl = { url ->
|
||||||
viewModel.onKindUrlClick(sourceUrl, url, kind.title)
|
onOpenKind(sourceUrl, url, kind.title)
|
||||||
},
|
},
|
||||||
onRefreshKinds = {
|
onRefreshKinds = {
|
||||||
viewModel.refreshButtonGroup(globalId)
|
onRefreshKinds(globalId)
|
||||||
},
|
},
|
||||||
useCase = useCase,
|
useCase = useCase,
|
||||||
isMiuix = isMiuix,
|
isMiuix = isMiuix,
|
||||||
|
|||||||
@@ -1022,6 +1022,10 @@
|
|||||||
<string name="hide_when_status_bar_show">状态栏显示时隐藏</string>
|
<string name="hide_when_status_bar_show">状态栏显示时隐藏</string>
|
||||||
<string name="reverse_toc">反转目录</string>
|
<string name="reverse_toc">反转目录</string>
|
||||||
<string name="show_discovery">显示发现</string>
|
<string name="show_discovery">显示发现</string>
|
||||||
|
<string name="show_home">显示首页</string>
|
||||||
|
<string name="main_navigation_settings">首页与导航</string>
|
||||||
|
<string name="main_navigation_settings_summary">配置首页、发现、订阅和默认主页</string>
|
||||||
|
<string name="navigation_order">导航顺序</string>
|
||||||
<string name="style">样式</string>
|
<string name="style">样式</string>
|
||||||
<string name="group_style">分组样式</string>
|
<string name="group_style">分组样式</string>
|
||||||
<string name="export_file_name">导出文件名</string>
|
<string name="export_file_name">导出文件名</string>
|
||||||
@@ -1830,12 +1834,9 @@
|
|||||||
<string name="homepage_type_label">类型</string>
|
<string name="homepage_type_label">类型</string>
|
||||||
<string name="homepage_layout_config_label">布局配置</string>
|
<string name="homepage_layout_config_label">布局配置</string>
|
||||||
<string name="homepage_edit_raw_json">编辑原始 JSON (LayoutConfig)</string>
|
<string name="homepage_edit_raw_json">编辑原始 JSON (LayoutConfig)</string>
|
||||||
<string name="homepage_layout_settings">布局设置</string>
|
|
||||||
<string name="discovery_modules">发现模块</string>
|
|
||||||
<string name="classic_discovery">经典发现</string>
|
|
||||||
<string name="switch_to_discovery_modules">切换至发现模块</string>
|
|
||||||
<string name="switch_to_classic_discovery">切换至经典发现</string>
|
|
||||||
<string name="home_recent_reading">最近阅读</string>
|
<string name="home_recent_reading">最近阅读</string>
|
||||||
|
<string name="home_recent_books">最近阅读记录</string>
|
||||||
|
<string name="home_dashboard_settings">首页组件</string>
|
||||||
<string name="home_no_recent_reading">暂无阅读记录</string>
|
<string name="home_no_recent_reading">暂无阅读记录</string>
|
||||||
<string name="home_total_read_books">累计阅读</string>
|
<string name="home_total_read_books">累计阅读</string>
|
||||||
<string name="home_total_reading_time">阅读时长</string>
|
<string name="home_total_reading_time">阅读时长</string>
|
||||||
@@ -1851,14 +1852,10 @@
|
|||||||
<string name="home_latest_backup_value">最近备份:%1$s</string>
|
<string name="home_latest_backup_value">最近备份:%1$s</string>
|
||||||
<string name="home_no_webdav_backup">暂无 WebDAV 备份</string>
|
<string name="home_no_webdav_backup">暂无 WebDAV 备份</string>
|
||||||
<string name="home_loading_webdav_backup">正在获取备份信息…</string>
|
<string name="home_loading_webdav_backup">正在获取备份信息…</string>
|
||||||
|
<string name="home_webdav_backup_load_error">无法获取备份信息</string>
|
||||||
<string name="home_webdav_backup_confirmation">立即将当前数据备份到 WebDAV?</string>
|
<string name="home_webdav_backup_confirmation">立即将当前数据备份到 WebDAV?</string>
|
||||||
<string name="home_webdav_restore_confirmation">恢复 WebDAV 备份 %1$s?当前数据可能被覆盖。</string>
|
<string name="home_webdav_restore_confirmation">恢复 WebDAV 备份 %1$s?当前数据可能被覆盖。</string>
|
||||||
<string name="homepage_layout_mode">首页布局模式</string>
|
|
||||||
<string name="homepage_layout_mixed">混合列表</string>
|
|
||||||
<string name="homepage_layout_tabs">分源Tab</string>
|
|
||||||
<string name="homepage_filter_sources">筛选书源</string>
|
|
||||||
<string name="homepage_no_source_sets_selected">未选择任何书源集</string>
|
<string name="homepage_no_source_sets_selected">未选择任何书源集</string>
|
||||||
<string name="homepage_module_error">模块错误</string>
|
|
||||||
<string name="homepage_add_module_definition">请在书源中添加首页模块定义</string>
|
<string name="homepage_add_module_definition">请在书源中添加首页模块定义</string>
|
||||||
<string name="homepage_collapse">收起</string>
|
<string name="homepage_collapse">收起</string>
|
||||||
<string name="homepage_show_all">显示全部</string>
|
<string name="homepage_show_all">显示全部</string>
|
||||||
|
|||||||
@@ -895,6 +895,10 @@
|
|||||||
<string name="diy_edit_source_group">輸入自訂源分組名稱</string>
|
<string name="diy_edit_source_group">輸入自訂源分組名稱</string>
|
||||||
<string name="reverse_toc">反轉目錄</string>
|
<string name="reverse_toc">反轉目錄</string>
|
||||||
<string name="show_discovery">顯示發現</string>
|
<string name="show_discovery">顯示發現</string>
|
||||||
|
<string name="show_home">顯示首頁</string>
|
||||||
|
<string name="main_navigation_settings">首頁與導覽</string>
|
||||||
|
<string name="main_navigation_settings_summary">設定首頁、發現、訂閱和預設主頁</string>
|
||||||
|
<string name="navigation_order">導覽順序</string>
|
||||||
<string name="style">樣式</string>
|
<string name="style">樣式</string>
|
||||||
<string name="group_style">分組樣式</string>
|
<string name="group_style">分組樣式</string>
|
||||||
<string name="export_file_name">導出文件名</string>
|
<string name="export_file_name">導出文件名</string>
|
||||||
@@ -1364,12 +1368,9 @@
|
|||||||
<string name="homepage_type_label">類型</string>
|
<string name="homepage_type_label">類型</string>
|
||||||
<string name="homepage_layout_config_label">佈局配置</string>
|
<string name="homepage_layout_config_label">佈局配置</string>
|
||||||
<string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
|
<string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
|
||||||
<string name="homepage_layout_settings">佈局設置</string>
|
|
||||||
<string name="discovery_modules">發現模塊</string>
|
|
||||||
<string name="classic_discovery">經典發現</string>
|
|
||||||
<string name="switch_to_discovery_modules">切換至發現模塊</string>
|
|
||||||
<string name="switch_to_classic_discovery">切換至經典發現</string>
|
|
||||||
<string name="home_recent_reading">最近閱讀</string>
|
<string name="home_recent_reading">最近閱讀</string>
|
||||||
|
<string name="home_recent_books">最近閱讀記錄</string>
|
||||||
|
<string name="home_dashboard_settings">首頁組件</string>
|
||||||
<string name="home_no_recent_reading">暫無閱讀記錄</string>
|
<string name="home_no_recent_reading">暫無閱讀記錄</string>
|
||||||
<string name="home_total_read_books">累計閱讀</string>
|
<string name="home_total_read_books">累計閱讀</string>
|
||||||
<string name="home_total_reading_time">閱讀時長</string>
|
<string name="home_total_reading_time">閱讀時長</string>
|
||||||
@@ -1385,14 +1386,10 @@
|
|||||||
<string name="home_latest_backup_value">最近備份:%1$s</string>
|
<string name="home_latest_backup_value">最近備份:%1$s</string>
|
||||||
<string name="home_no_webdav_backup">暫無 WebDAV 備份</string>
|
<string name="home_no_webdav_backup">暫無 WebDAV 備份</string>
|
||||||
<string name="home_loading_webdav_backup">正在獲取備份資訊…</string>
|
<string name="home_loading_webdav_backup">正在獲取備份資訊…</string>
|
||||||
|
<string name="home_webdav_backup_load_error">無法獲取備份資訊</string>
|
||||||
<string name="home_webdav_backup_confirmation">立即將目前資料備份到 WebDAV?</string>
|
<string name="home_webdav_backup_confirmation">立即將目前資料備份到 WebDAV?</string>
|
||||||
<string name="home_webdav_restore_confirmation">恢復 WebDAV 備份 %1$s?目前資料可能被覆蓋。</string>
|
<string name="home_webdav_restore_confirmation">恢復 WebDAV 備份 %1$s?目前資料可能被覆蓋。</string>
|
||||||
<string name="homepage_layout_mode">首頁佈局模式</string>
|
|
||||||
<string name="homepage_layout_mixed">混合列表</string>
|
|
||||||
<string name="homepage_layout_tabs">分源Tab</string>
|
|
||||||
<string name="homepage_filter_sources">篩選書源</string>
|
|
||||||
<string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
|
<string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
|
||||||
<string name="homepage_module_error">模塊錯誤</string>
|
|
||||||
<string name="homepage_add_module_definition">請在書源中添加首頁模塊定義</string>
|
<string name="homepage_add_module_definition">請在書源中添加首頁模塊定義</string>
|
||||||
<string name="homepage_collapse">收起</string>
|
<string name="homepage_collapse">收起</string>
|
||||||
<string name="homepage_show_all">顯示全部</string>
|
<string name="homepage_show_all">顯示全部</string>
|
||||||
|
|||||||
@@ -898,6 +898,10 @@
|
|||||||
<string name="hide_when_status_bar_show">狀態欄顯示時隱藏</string>
|
<string name="hide_when_status_bar_show">狀態欄顯示時隱藏</string>
|
||||||
<string name="reverse_toc">反轉目錄</string>
|
<string name="reverse_toc">反轉目錄</string>
|
||||||
<string name="show_discovery">顯示發現</string>
|
<string name="show_discovery">顯示發現</string>
|
||||||
|
<string name="show_home">顯示首頁</string>
|
||||||
|
<string name="main_navigation_settings">首頁與導覽</string>
|
||||||
|
<string name="main_navigation_settings_summary">設定首頁、發現、訂閱和預設主頁</string>
|
||||||
|
<string name="navigation_order">導覽順序</string>
|
||||||
<string name="style">樣式</string>
|
<string name="style">樣式</string>
|
||||||
<string name="group_style">分組樣式</string>
|
<string name="group_style">分組樣式</string>
|
||||||
<string name="export_file_name">匯出檔案名</string>
|
<string name="export_file_name">匯出檔案名</string>
|
||||||
@@ -1368,12 +1372,9 @@
|
|||||||
<string name="homepage_type_label">類型</string>
|
<string name="homepage_type_label">類型</string>
|
||||||
<string name="homepage_layout_config_label">佈局配置</string>
|
<string name="homepage_layout_config_label">佈局配置</string>
|
||||||
<string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
|
<string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
|
||||||
<string name="homepage_layout_settings">佈局設置</string>
|
|
||||||
<string name="discovery_modules">發現模組</string>
|
|
||||||
<string name="classic_discovery">經典發現</string>
|
|
||||||
<string name="switch_to_discovery_modules">切換至發現模組</string>
|
|
||||||
<string name="switch_to_classic_discovery">切換至經典發現</string>
|
|
||||||
<string name="home_recent_reading">最近閱讀</string>
|
<string name="home_recent_reading">最近閱讀</string>
|
||||||
|
<string name="home_recent_books">最近閱讀記錄</string>
|
||||||
|
<string name="home_dashboard_settings">首頁元件</string>
|
||||||
<string name="home_no_recent_reading">暫無閱讀記錄</string>
|
<string name="home_no_recent_reading">暫無閱讀記錄</string>
|
||||||
<string name="home_total_read_books">累計閱讀</string>
|
<string name="home_total_read_books">累計閱讀</string>
|
||||||
<string name="home_total_reading_time">閱讀時長</string>
|
<string name="home_total_reading_time">閱讀時長</string>
|
||||||
@@ -1389,14 +1390,10 @@
|
|||||||
<string name="home_latest_backup_value">最近備份:%1$s</string>
|
<string name="home_latest_backup_value">最近備份:%1$s</string>
|
||||||
<string name="home_no_webdav_backup">暫無 WebDAV 備份</string>
|
<string name="home_no_webdav_backup">暫無 WebDAV 備份</string>
|
||||||
<string name="home_loading_webdav_backup">正在取得備份資訊…</string>
|
<string name="home_loading_webdav_backup">正在取得備份資訊…</string>
|
||||||
|
<string name="home_webdav_backup_load_error">無法取得備份資訊</string>
|
||||||
<string name="home_webdav_backup_confirmation">立即將目前資料備份到 WebDAV?</string>
|
<string name="home_webdav_backup_confirmation">立即將目前資料備份到 WebDAV?</string>
|
||||||
<string name="home_webdav_restore_confirmation">還原 WebDAV 備份 %1$s?目前資料可能被覆蓋。</string>
|
<string name="home_webdav_restore_confirmation">還原 WebDAV 備份 %1$s?目前資料可能被覆蓋。</string>
|
||||||
<string name="homepage_layout_mode">首頁佈局模式</string>
|
|
||||||
<string name="homepage_layout_mixed">混合列表</string>
|
|
||||||
<string name="homepage_layout_tabs">分源Tab</string>
|
|
||||||
<string name="homepage_filter_sources">篩選書源</string>
|
|
||||||
<string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
|
<string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
|
||||||
<string name="homepage_module_error">模組錯誤</string>
|
|
||||||
<string name="homepage_add_module_definition">請在書源中添加首頁模組定義</string>
|
<string name="homepage_add_module_definition">請在書源中添加首頁模組定義</string>
|
||||||
<string name="homepage_collapse">收起</string>
|
<string name="homepage_collapse">收起</string>
|
||||||
<string name="homepage_show_all">顯示全部</string>
|
<string name="homepage_show_all">顯示全部</string>
|
||||||
|
|||||||
@@ -260,8 +260,7 @@
|
|||||||
<string-array name="default_home_page">
|
<string-array name="default_home_page">
|
||||||
<item>@string/home</item>
|
<item>@string/home</item>
|
||||||
<item>@string/bookshelf</item>
|
<item>@string/bookshelf</item>
|
||||||
<item>@string/discovery_modules</item>
|
<item>@string/discovery</item>
|
||||||
<item>@string/classic_discovery</item>
|
|
||||||
<item>@string/rss</item>
|
<item>@string/rss</item>
|
||||||
<item>@string/my</item>
|
<item>@string/my</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
@@ -277,7 +276,6 @@
|
|||||||
<string-array name="default_home_page_value">
|
<string-array name="default_home_page_value">
|
||||||
<item>home</item>
|
<item>home</item>
|
||||||
<item>bookshelf</item>
|
<item>bookshelf</item>
|
||||||
<item>explore_modules</item>
|
|
||||||
<item>explore</item>
|
<item>explore</item>
|
||||||
<item>rss</item>
|
<item>rss</item>
|
||||||
<item>my</item>
|
<item>my</item>
|
||||||
|
|||||||
@@ -1053,6 +1053,10 @@
|
|||||||
<string name="hide_when_status_bar_show">Hide when status bar show</string>
|
<string name="hide_when_status_bar_show">Hide when status bar show</string>
|
||||||
<string name="reverse_toc">Reverse toc</string>
|
<string name="reverse_toc">Reverse toc</string>
|
||||||
<string name="show_discovery">Show Discovery</string>
|
<string name="show_discovery">Show Discovery</string>
|
||||||
|
<string name="show_home">Show Home</string>
|
||||||
|
<string name="main_navigation_settings">Home and navigation</string>
|
||||||
|
<string name="main_navigation_settings_summary">Configure Home, Discovery, Subscriptions, and the start page</string>
|
||||||
|
<string name="navigation_order">Navigation order</string>
|
||||||
<string name="style">Style</string>
|
<string name="style">Style</string>
|
||||||
<string name="group_style">Group style</string>
|
<string name="group_style">Group style</string>
|
||||||
<string name="export_file_name">Export file name</string>
|
<string name="export_file_name">Export file name</string>
|
||||||
@@ -1963,12 +1967,9 @@
|
|||||||
<string name="homepage_type_label">Type</string>
|
<string name="homepage_type_label">Type</string>
|
||||||
<string name="homepage_layout_config_label">Layout Config</string>
|
<string name="homepage_layout_config_label">Layout Config</string>
|
||||||
<string name="homepage_edit_raw_json">Edit Raw JSON (LayoutConfig)</string>
|
<string name="homepage_edit_raw_json">Edit Raw JSON (LayoutConfig)</string>
|
||||||
<string name="homepage_layout_settings">Layout Settings</string>
|
|
||||||
<string name="discovery_modules">Discovery Modules</string>
|
|
||||||
<string name="classic_discovery">Classic Discovery</string>
|
|
||||||
<string name="switch_to_discovery_modules">Switch to Discovery Modules</string>
|
|
||||||
<string name="switch_to_classic_discovery">Switch to Classic Discovery</string>
|
|
||||||
<string name="home_recent_reading">Recently Reading</string>
|
<string name="home_recent_reading">Recently Reading</string>
|
||||||
|
<string name="home_recent_books">Recent reading history</string>
|
||||||
|
<string name="home_dashboard_settings">Home components</string>
|
||||||
<string name="home_no_recent_reading">No reading history yet</string>
|
<string name="home_no_recent_reading">No reading history yet</string>
|
||||||
<string name="home_total_read_books">Books Read</string>
|
<string name="home_total_read_books">Books Read</string>
|
||||||
<string name="home_total_reading_time">Reading Time</string>
|
<string name="home_total_reading_time">Reading Time</string>
|
||||||
@@ -1984,14 +1985,10 @@
|
|||||||
<string name="home_latest_backup_value">Latest backup: %1$s</string>
|
<string name="home_latest_backup_value">Latest backup: %1$s</string>
|
||||||
<string name="home_no_webdav_backup">No WebDAV backup found</string>
|
<string name="home_no_webdav_backup">No WebDAV backup found</string>
|
||||||
<string name="home_loading_webdav_backup">Loading backup information…</string>
|
<string name="home_loading_webdav_backup">Loading backup information…</string>
|
||||||
|
<string name="home_webdav_backup_load_error">Couldn\'t load backup information</string>
|
||||||
<string name="home_webdav_backup_confirmation">Back up current data to WebDAV now?</string>
|
<string name="home_webdav_backup_confirmation">Back up current data to WebDAV now?</string>
|
||||||
<string name="home_webdav_restore_confirmation">Restore WebDAV backup %1$s? Current data may be overwritten.</string>
|
<string name="home_webdav_restore_confirmation">Restore WebDAV backup %1$s? Current data may be overwritten.</string>
|
||||||
<string name="homepage_layout_mode">Homepage Layout Mode</string>
|
|
||||||
<string name="homepage_layout_mixed">Mixed List</string>
|
|
||||||
<string name="homepage_layout_tabs">Tabs by Source</string>
|
|
||||||
<string name="homepage_filter_sources">Filter Sources</string>
|
|
||||||
<string name="homepage_no_source_sets_selected">No source sets selected</string>
|
<string name="homepage_no_source_sets_selected">No source sets selected</string>
|
||||||
<string name="homepage_module_error">Module Error</string>
|
|
||||||
<string name="homepage_add_module_definition">Please add homepage module definitions in the book source</string>
|
<string name="homepage_add_module_definition">Please add homepage module definitions in the book source</string>
|
||||||
<string name="homepage_collapse">Collapse</string>
|
<string name="homepage_collapse">Collapse</string>
|
||||||
<string name="homepage_show_all">Show All</string>
|
<string name="homepage_show_all">Show All</string>
|
||||||
|
|||||||
Reference in new issue
Block a user