更加智能的阅读记录逻辑
This commit is contained in:
@@ -92,6 +92,19 @@ interface ReadRecordDao {
|
|||||||
@Query("DELETE FROM readRecordDetail WHERE bookName = :bookName")
|
@Query("DELETE FROM readRecordDetail WHERE bookName = :bookName")
|
||||||
fun deleteDetailByName(bookName: String)
|
fun deleteDetailByName(bookName: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定书籍的最后一条阅读会话
|
||||||
|
* 用于判断是否可以合并
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM readRecordSession WHERE bookName = :bookName ORDER BY endTime DESC LIMIT 1")
|
||||||
|
suspend fun getLatestSessionByBook(bookName: String): ReadRecordSession?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新现有的会话
|
||||||
|
*/
|
||||||
|
@Update
|
||||||
|
suspend fun updateSession(session: ReadRecordSession)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插入阅读会话记录。
|
* 插入阅读会话记录。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,10 +15,64 @@ class ReadRecordRepository(
|
|||||||
private val dao: ReadRecordDao
|
private val dao: ReadRecordDao
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 2分钟内重新打开书本,视为同一次阅读
|
||||||
|
private const val CONTINUE_THRESHOLD = 120 * 1000L
|
||||||
|
// 会话总时长小于10秒则不记录
|
||||||
|
private const val MIN_READ_DURATION = 0 * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCurrentDeviceId(): String = ""
|
private fun getCurrentDeviceId(): String = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存一个完整的阅读会话,并同步更新 ReadRecordDetail 和 ReadRecord。
|
* 智能保存阅读会话。
|
||||||
|
* 自动判断是插入新会话还是合并到上一次会话
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
suspend fun saveOrMergeReadSession(newSession: ReadRecordSession) {
|
||||||
|
|
||||||
|
val segmentDuration = newSession.endTime - newSession.startTime
|
||||||
|
|
||||||
|
if (segmentDuration < MIN_READ_DURATION) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestSession = dao.getLatestSessionByBook(newSession.bookName)
|
||||||
|
|
||||||
|
if (latestSession != null) {
|
||||||
|
val timeGap = newSession.startTime - latestSession.endTime
|
||||||
|
|
||||||
|
if (timeGap <= CONTINUE_THRESHOLD && timeGap >= 0) {
|
||||||
|
|
||||||
|
val durationDelta = segmentDuration
|
||||||
|
val wordsDelta = newSession.words
|
||||||
|
|
||||||
|
val mergedSession = latestSession.copy(
|
||||||
|
endTime = newSession.endTime,
|
||||||
|
words = latestSession.words + wordsDelta
|
||||||
|
)
|
||||||
|
|
||||||
|
dao.updateSession(mergedSession)
|
||||||
|
|
||||||
|
val dateString = DateUtil.format(Date(mergedSession.startTime), DatePattern.NORM_DATE_PATTERN)
|
||||||
|
updateReadRecordDetail(mergedSession, durationDelta, wordsDelta, dateString)
|
||||||
|
updateReadRecord(mergedSession, durationDelta)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理新会话
|
||||||
|
dao.insertSession(newSession)
|
||||||
|
|
||||||
|
val dateString = DateUtil.format(Date(newSession.startTime), DatePattern.NORM_DATE_PATTERN)
|
||||||
|
updateReadRecordDetail(newSession, segmentDuration, newSession.words, dateString)
|
||||||
|
updateReadRecord(newSession, segmentDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存一个完整的阅读会话
|
||||||
|
* 没几把用
|
||||||
*/
|
*/
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun saveReadSession(session: ReadRecordSession) {
|
suspend fun saveReadSession(session: ReadRecordSession) {
|
||||||
@@ -26,17 +80,23 @@ class ReadRecordRepository(
|
|||||||
|
|
||||||
val sessionDuration = session.endTime - session.startTime
|
val sessionDuration = session.endTime - session.startTime
|
||||||
val dateString = DateUtil.format(Date(session.startTime), DatePattern.NORM_DATE_PATTERN)
|
val dateString = DateUtil.format(Date(session.startTime), DatePattern.NORM_DATE_PATTERN)
|
||||||
|
updateReadRecordDetail(session, sessionDuration, session.words, dateString)
|
||||||
|
|
||||||
updateReadRecordDetail(session, sessionDuration, dateString)
|
|
||||||
updateReadRecord(session, sessionDuration)
|
updateReadRecord(session, sessionDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateReadRecord(session: ReadRecordSession, sessionDuration: Long) {
|
/**
|
||||||
|
* 更新总记录表 (ReadRecord)
|
||||||
|
* @param durationDelta 增加的时长
|
||||||
|
*/
|
||||||
|
private suspend fun updateReadRecord(session: ReadRecordSession, durationDelta: Long) {
|
||||||
|
if (durationDelta <= 0) return
|
||||||
|
|
||||||
val existingRecord = dao.getReadRecord(session.deviceId, session.bookName)
|
val existingRecord = dao.getReadRecord(session.deviceId, session.bookName)
|
||||||
|
|
||||||
if (existingRecord != null) {
|
if (existingRecord != null) {
|
||||||
val updatedRecord = existingRecord.copy(
|
val updatedRecord = existingRecord.copy(
|
||||||
readTime = existingRecord.readTime + sessionDuration,
|
readTime = existingRecord.readTime + durationDelta,
|
||||||
lastRead = session.endTime
|
lastRead = session.endTime
|
||||||
)
|
)
|
||||||
dao.update(updatedRecord)
|
dao.update(updatedRecord)
|
||||||
@@ -44,19 +104,31 @@ class ReadRecordRepository(
|
|||||||
val newRecord = ReadRecord(
|
val newRecord = ReadRecord(
|
||||||
deviceId = session.deviceId,
|
deviceId = session.deviceId,
|
||||||
bookName = session.bookName,
|
bookName = session.bookName,
|
||||||
readTime = sessionDuration,
|
readTime = durationDelta,
|
||||||
lastRead = session.endTime
|
lastRead = session.endTime
|
||||||
)
|
)
|
||||||
dao.insert(newRecord)
|
dao.insert(newRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateReadRecordDetail(session: ReadRecordSession, sessionDuration: Long, dateString: String) {
|
/**
|
||||||
|
* 更新每日详情表 (ReadRecordDetail)
|
||||||
|
* @param durationDelta 增加的时长
|
||||||
|
* @param wordsDelta 增加的字数
|
||||||
|
*/
|
||||||
|
private suspend fun updateReadRecordDetail(
|
||||||
|
session: ReadRecordSession,
|
||||||
|
durationDelta: Long,
|
||||||
|
wordsDelta: Long,
|
||||||
|
dateString: String
|
||||||
|
) {
|
||||||
|
if (durationDelta <= 0 && wordsDelta <= 0) return
|
||||||
|
|
||||||
val existingDetail = dao.getDetail(session.deviceId, session.bookName, dateString)
|
val existingDetail = dao.getDetail(session.deviceId, session.bookName, dateString)
|
||||||
|
|
||||||
if (existingDetail != null) {
|
if (existingDetail != null) {
|
||||||
existingDetail.readTime += sessionDuration
|
existingDetail.readTime += durationDelta
|
||||||
existingDetail.readWords += session.words
|
existingDetail.readWords += wordsDelta
|
||||||
existingDetail.firstReadTime = min(existingDetail.firstReadTime, session.startTime)
|
existingDetail.firstReadTime = min(existingDetail.firstReadTime, session.startTime)
|
||||||
existingDetail.lastReadTime = max(existingDetail.lastReadTime, session.endTime)
|
existingDetail.lastReadTime = max(existingDetail.lastReadTime, session.endTime)
|
||||||
dao.insertDetail(existingDetail)
|
dao.insertDetail(existingDetail)
|
||||||
@@ -65,8 +137,8 @@ class ReadRecordRepository(
|
|||||||
deviceId = session.deviceId,
|
deviceId = session.deviceId,
|
||||||
bookName = session.bookName,
|
bookName = session.bookName,
|
||||||
date = dateString,
|
date = dateString,
|
||||||
readTime = sessionDuration,
|
readTime = durationDelta,
|
||||||
readWords = session.words,
|
readWords = wordsDelta,
|
||||||
firstReadTime = session.startTime,
|
firstReadTime = session.startTime,
|
||||||
lastReadTime = session.endTime
|
lastReadTime = session.endTime
|
||||||
)
|
)
|
||||||
@@ -115,5 +187,4 @@ class ReadRecordRepository(
|
|||||||
// 暴露总时长
|
// 暴露总时长
|
||||||
val allTime: Long
|
val allTime: Long
|
||||||
get() = dao.allTime
|
get() = dao.allTime
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
@@ -99,8 +100,20 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
var contentProcessor: ContentProcessor? = null
|
var contentProcessor: ContentProcessor? = null
|
||||||
val downloadScope = CoroutineScope(SupervisorJob() + IO)
|
val downloadScope = CoroutineScope(SupervisorJob() + IO)
|
||||||
val preDownloadSemaphore = Semaphore(2)
|
val preDownloadSemaphore = Semaphore(2)
|
||||||
|
|
||||||
val executor = globalExecutor
|
val executor = globalExecutor
|
||||||
|
|
||||||
|
private val ioScope = CoroutineScope(IO)
|
||||||
|
|
||||||
|
private var autoSaveJob: Job? = null
|
||||||
|
|
||||||
|
private var currentActiveSession: ReadRecordSession? = null
|
||||||
|
//占位
|
||||||
|
private var currentReadLength: Long = 10L
|
||||||
|
private const val AUTO_SAVE_INTERVAL = 120 * 1000L
|
||||||
|
|
||||||
|
private const val MIN_READ_DURATION = 10 * 1000L
|
||||||
|
|
||||||
fun resetData(book: Book) {
|
fun resetData(book: Book) {
|
||||||
ReadBook.book = book
|
ReadBook.book = book
|
||||||
readRecord.bookName = book.name
|
readRecord.bookName = book.name
|
||||||
@@ -286,91 +299,84 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阅读暂停的阈值
|
fun initReadTime() {
|
||||||
private const val PAUSE_THRESHOLD = 5 * 60 * 1000L
|
|
||||||
// 存储当前正在累积的阅读会话对象
|
|
||||||
private var currentActiveSession: ReadRecordSession? = null
|
|
||||||
//占位
|
|
||||||
private var currentReadLength: Long = 10L
|
|
||||||
|
|
||||||
fun upReadTime() {
|
|
||||||
val currentLength = currentReadLength
|
|
||||||
if (!AppConfig.enableReadRecord) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val currentBookName = book?.name ?: return
|
val currentBookName = book?.name ?: return
|
||||||
val endTime = System.currentTimeMillis()
|
if (currentActiveSession != null && currentActiveSession!!.bookName != currentBookName) {
|
||||||
|
commitReadSession()
|
||||||
//计算本次片段的时长和字数
|
|
||||||
val duration = endTime - readStartTime
|
|
||||||
var wordChange = currentLength - lastReadLength
|
|
||||||
if (wordChange < 0) wordChange = 0
|
|
||||||
|
|
||||||
//过滤无效记录
|
|
||||||
if (duration < 1000L && wordChange == 0L) {
|
|
||||||
readStartTime = endTime
|
|
||||||
lastReadLength = currentLength
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//检查是否中断
|
|
||||||
if (currentActiveSession != null) {
|
|
||||||
// 计算从上一个片段结束到当前片段开始的时间间隔
|
|
||||||
val timeSinceLastUpdate = readStartTime - currentActiveSession!!.endTime
|
|
||||||
|
|
||||||
if (timeSinceLastUpdate > PAUSE_THRESHOLD) {
|
|
||||||
// 如果间隔时间过长,说明用户暂停阅读后又回来了,提交旧会话
|
|
||||||
commitReadSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//累加或创建新会话
|
|
||||||
if (currentActiveSession == null) {
|
if (currentActiveSession == null) {
|
||||||
// 创建新的会话对象,使用 readStartTime 作为整个会话的起始时间
|
lastReadLength = currentReadLength
|
||||||
currentActiveSession = ReadRecordSession(
|
currentActiveSession = ReadRecordSession(
|
||||||
deviceId = "",
|
deviceId = "",
|
||||||
bookName = currentBookName,
|
bookName = currentBookName,
|
||||||
startTime = readStartTime,
|
startTime = readStartTime,
|
||||||
endTime = endTime,
|
endTime = readStartTime,
|
||||||
words = 0
|
words = 0
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// 累加数据:只更新结束时间和总字数
|
|
||||||
currentActiveSession = currentActiveSession!!.copy(
|
|
||||||
endTime = endTime,
|
|
||||||
words = currentActiveSession!!.words + wordChange
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upReadTime() {
|
||||||
|
val currentLength = currentReadLength
|
||||||
|
val currentBookName = book?.name ?: return
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (currentActiveSession == null || currentActiveSession!!.bookName != currentBookName) {
|
||||||
|
initReadTime()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wordChange = currentLength - lastReadLength
|
||||||
|
if (wordChange < 0) wordChange = 0
|
||||||
|
|
||||||
|
currentActiveSession = currentActiveSession!!.copy(
|
||||||
|
endTime = endTime,
|
||||||
|
words = currentActiveSession!!.words + wordChange
|
||||||
|
)
|
||||||
|
|
||||||
readStartTime = endTime
|
readStartTime = endTime
|
||||||
lastReadLength = currentLength
|
lastReadLength = currentLength
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun startAutoSaveSession() {
|
||||||
* 将当前累积的阅读会话(ReadRecordSession)写入数据库,并重置状态。
|
autoSaveJob?.cancel()
|
||||||
* 此方法必须在后台线程上调用。
|
autoSaveJob = ioScope.launch {
|
||||||
*/
|
while (isActive) {
|
||||||
fun commitReadSession() {
|
delay(AUTO_SAVE_INTERVAL)
|
||||||
// 检查是否有需要保存的数据
|
commitSessionInternal()
|
||||||
val sessionToSave = currentActiveSession ?: return
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 确保时长和字数有效
|
fun stopAutoSaveSession() {
|
||||||
if (sessionToSave.words <= 0 && (sessionToSave.endTime - sessionToSave.startTime) < 10L) {
|
autoSaveJob?.cancel()
|
||||||
|
autoSaveJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commitReadSession() {
|
||||||
|
ioScope.launch {
|
||||||
|
commitSessionInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部提交逻辑
|
||||||
|
*/
|
||||||
|
private suspend fun commitSessionInternal() {
|
||||||
|
val sessionToSave = currentActiveSession ?: return
|
||||||
|
val sessionDuration = sessionToSave.endTime - sessionToSave.startTime
|
||||||
|
if (sessionDuration < MIN_READ_DURATION) {
|
||||||
currentActiveSession = null
|
currentActiveSession = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//使用 runBlocking 确保数据库保存操作在当前线程上同步完成
|
|
||||||
try {
|
try {
|
||||||
kotlinx.coroutines.runBlocking {
|
readRecordRepository.saveOrMergeReadSession(sessionToSave)
|
||||||
readRecordRepository.saveReadSession(sessionToSave)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
|
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
|
||||||
|
} finally {
|
||||||
|
currentActiveSession = null
|
||||||
}
|
}
|
||||||
|
|
||||||
currentActiveSession = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upMsg(msg: String?) {
|
fun upMsg(msg: String?) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@@ -75,6 +76,16 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
|
|||||||
|
|
||||||
private val readRecordRepository: ReadRecordRepository by inject()
|
private val readRecordRepository: ReadRecordRepository by inject()
|
||||||
|
|
||||||
|
private val ioScope = CoroutineScope(IO)
|
||||||
|
|
||||||
|
private var autoSaveJob: Job? = null
|
||||||
|
|
||||||
|
private var currentActiveSession: ReadRecordSession? = null
|
||||||
|
private var currentReadLength: Long = 10L
|
||||||
|
private const val AUTO_SAVE_INTERVAL = 120 * 1000L
|
||||||
|
|
||||||
|
private const val MIN_READ_DURATION = 10 * 1000L
|
||||||
|
|
||||||
private var lastReadLength: Long = 0
|
private var lastReadLength: Long = 0
|
||||||
fun resetData(book: Book) {
|
fun resetData(book: Book) {
|
||||||
ReadManga.book = book
|
ReadManga.book = book
|
||||||
@@ -134,92 +145,84 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
|
|||||||
nextMangaChapter = null
|
nextMangaChapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
//每次切换章节更新阅读记录
|
fun initReadTime() {
|
||||||
// 阅读暂停的阈值
|
|
||||||
private const val PAUSE_THRESHOLD = 5 * 60 * 1000L
|
|
||||||
// 存储当前正在累积的阅读会话对象
|
|
||||||
private var currentActiveSession: ReadRecordSession? = null
|
|
||||||
//占位
|
|
||||||
private var currentReadLength: Long = 10L
|
|
||||||
|
|
||||||
fun upReadTime() {
|
|
||||||
val currentLength = currentReadLength
|
|
||||||
if (!AppConfig.enableReadRecord) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val currentBookName = book?.name ?: return
|
val currentBookName = book?.name ?: return
|
||||||
val endTime = System.currentTimeMillis()
|
if (currentActiveSession != null && currentActiveSession!!.bookName != currentBookName) {
|
||||||
|
commitReadSession()
|
||||||
//计算本次片段的时长和字数
|
|
||||||
val duration = endTime - readStartTime
|
|
||||||
var wordChange = currentLength - lastReadLength
|
|
||||||
if (wordChange < 0) wordChange = 0
|
|
||||||
|
|
||||||
//过滤无效记录
|
|
||||||
if (duration < 1000L && wordChange == 0L) {
|
|
||||||
readStartTime = endTime
|
|
||||||
lastReadLength = currentLength
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//检查是否中断
|
|
||||||
if (currentActiveSession != null) {
|
|
||||||
// 计算从上一个片段结束到当前片段开始的时间间隔
|
|
||||||
val timeSinceLastUpdate = readStartTime - currentActiveSession!!.endTime
|
|
||||||
|
|
||||||
if (timeSinceLastUpdate > PAUSE_THRESHOLD) {
|
|
||||||
// 如果间隔时间过长,说明用户暂停阅读后又回来了,提交旧会话
|
|
||||||
commitReadSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//累加或创建新会话
|
|
||||||
if (currentActiveSession == null) {
|
if (currentActiveSession == null) {
|
||||||
// 创建新的会话对象,使用 readStartTime 作为整个会话的起始时间
|
lastReadLength = currentReadLength
|
||||||
currentActiveSession = ReadRecordSession(
|
currentActiveSession = ReadRecordSession(
|
||||||
deviceId = "",
|
deviceId = "",
|
||||||
bookName = currentBookName,
|
bookName = currentBookName,
|
||||||
startTime = readStartTime,
|
startTime = readStartTime,
|
||||||
endTime = endTime,
|
endTime = readStartTime,
|
||||||
words = 0
|
words = 0
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// 累加数据:只更新结束时间和总字数
|
|
||||||
currentActiveSession = currentActiveSession!!.copy(
|
|
||||||
endTime = endTime,
|
|
||||||
words = currentActiveSession!!.words + wordChange
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upReadTime() {
|
||||||
|
val currentLength = currentReadLength
|
||||||
|
val currentBookName = book?.name ?: return
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if (currentActiveSession == null || currentActiveSession!!.bookName != currentBookName) {
|
||||||
|
initReadTime()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wordChange = currentLength - lastReadLength
|
||||||
|
if (wordChange < 0) wordChange = 0
|
||||||
|
|
||||||
|
currentActiveSession = currentActiveSession!!.copy(
|
||||||
|
endTime = endTime,
|
||||||
|
words = currentActiveSession!!.words + wordChange
|
||||||
|
)
|
||||||
|
|
||||||
readStartTime = endTime
|
readStartTime = endTime
|
||||||
lastReadLength = currentLength
|
lastReadLength = currentLength
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun startAutoSaveSession() {
|
||||||
* 将当前累积的阅读会话(ReadRecordSession)写入数据库,并重置状态。
|
autoSaveJob?.cancel()
|
||||||
* 此方法必须在后台线程上调用。
|
autoSaveJob = ioScope.launch {
|
||||||
*/
|
while (isActive) {
|
||||||
fun commitReadSession() {
|
delay(AUTO_SAVE_INTERVAL)
|
||||||
// 检查是否有需要保存的数据
|
commitSessionInternal()
|
||||||
val sessionToSave = currentActiveSession ?: return
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 确保时长和字数有效
|
fun stopAutoSaveSession() {
|
||||||
if (sessionToSave.words <= 0 && (sessionToSave.endTime - sessionToSave.startTime) < 10L) {
|
autoSaveJob?.cancel()
|
||||||
|
autoSaveJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commitReadSession() {
|
||||||
|
ioScope.launch {
|
||||||
|
commitSessionInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部提交逻辑
|
||||||
|
*/
|
||||||
|
private suspend fun commitSessionInternal() {
|
||||||
|
val sessionToSave = currentActiveSession ?: return
|
||||||
|
val sessionDuration = sessionToSave.endTime - sessionToSave.startTime
|
||||||
|
if (sessionDuration < MIN_READ_DURATION) {
|
||||||
currentActiveSession = null
|
currentActiveSession = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//使用 runBlocking 确保数据库保存操作在当前线程上同步完成
|
|
||||||
try {
|
try {
|
||||||
kotlinx.coroutines.runBlocking {
|
readRecordRepository.saveOrMergeReadSession(sessionToSave)
|
||||||
readRecordRepository.saveReadSession(sessionToSave)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
|
AppLog.put("保存阅读会话出错: ${sessionToSave.bookName}", e)
|
||||||
|
} finally {
|
||||||
|
currentActiveSession = null
|
||||||
}
|
}
|
||||||
|
|
||||||
currentActiveSession = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|||||||
@@ -429,6 +429,8 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
if (enableAutoScroll) {
|
if (enableAutoScroll) {
|
||||||
mScrollTimer.isEnabled = true
|
mScrollTimer.isEnabled = true
|
||||||
}
|
}
|
||||||
|
ReadManga.initReadTime()
|
||||||
|
ReadManga.startAutoSaveSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -444,6 +446,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
Backup.autoBack(this)
|
Backup.autoBack(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ReadManga.stopAutoSaveSession()
|
||||||
ReadManga.commitReadSession()
|
ReadManga.commitReadSession()
|
||||||
ReadManga.cancelPreDownloadTask()
|
ReadManga.cancelPreDownloadTask()
|
||||||
networkChangedListener.unRegister()
|
networkChangedListener.unRegister()
|
||||||
|
|||||||
@@ -392,6 +392,8 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
ReadBook.readStartTime = System.currentTimeMillis()
|
ReadBook.readStartTime = System.currentTimeMillis()
|
||||||
|
ReadBook.initReadTime()
|
||||||
|
ReadBook.startAutoSaveSession()
|
||||||
if (bookChanged) {
|
if (bookChanged) {
|
||||||
bookChanged = false
|
bookChanged = false
|
||||||
ReadBook.callBack = this
|
ReadBook.callBack = this
|
||||||
@@ -423,6 +425,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
autoPageStop()
|
autoPageStop()
|
||||||
backupJob?.cancel()
|
backupJob?.cancel()
|
||||||
ReadBook.saveRead()
|
ReadBook.saveRead()
|
||||||
|
ReadBook.stopAutoSaveSession()
|
||||||
ReadBook.commitReadSession()
|
ReadBook.commitReadSession()
|
||||||
ReadBook.cancelPreDownloadTask()
|
ReadBook.cancelPreDownloadTask()
|
||||||
unregisterReceiver(timeBatteryReceiver)
|
unregisterReceiver(timeBatteryReceiver)
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ fun TimelineSessionItem(
|
|||||||
val timelineX = 24.dp
|
val timelineX = 24.dp
|
||||||
val contentPaddingStart = 32.dp
|
val contentPaddingStart = 32.dp
|
||||||
|
|
||||||
val lineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
val lineColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
val nodeColor = MaterialTheme.colorScheme.primary
|
val nodeColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -364,7 +364,7 @@ fun TimelineSessionItem(
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = startTimeText,
|
text = endTimeText,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -385,7 +385,8 @@ fun TimelineSessionItem(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
"时长: ${formatDuring(duration)}",
|
"时长: ${formatDuring(duration)}",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
// Text(
|
// Text(
|
||||||
// "字数: ${session.words}",
|
// "字数: ${session.words}",
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package io.legado.app.ui.widget.compose
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyMessageView(
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
faces: List<String> = listOf(
|
||||||
|
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
|
||||||
|
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
|
||||||
|
),
|
||||||
|
faceTextSize: TextUnit = 32.sp,
|
||||||
|
onFaceClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var currentFace by remember { mutableStateOf(faces.random()) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
AnimatedTextLine(
|
||||||
|
text = currentFace,
|
||||||
|
fontSize = faceTextSize,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
currentFace = faces.random()
|
||||||
|
onFaceClick?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
AnimatedTextLine(
|
||||||
|
text = message,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 2,
|
||||||
|
softWrap = true,
|
||||||
|
modifier = Modifier.widthIn(max = 240.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyMessageView(
|
||||||
|
@StringRes messageResId: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
faces: List<String> = listOf(
|
||||||
|
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
|
||||||
|
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
|
||||||
|
),
|
||||||
|
faceTextSize: TextUnit = 32.sp,
|
||||||
|
onFaceClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val message = stringResource(id = messageResId)
|
||||||
|
EmptyMessageView(
|
||||||
|
message = message,
|
||||||
|
modifier = modifier,
|
||||||
|
faces = faces,
|
||||||
|
faceTextSize = faceTextSize,
|
||||||
|
onFaceClick = onFaceClick
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user