Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
52801c3
🎨 add loading indicators in ImageViewer for better user feedback duri…
Whyjsee Mar 12, 2026
b53991e
🎨 add delete functionality to BookmarkDelegate and integrate with Bot…
Whyjsee Mar 12, 2026
ea1fe2f
🎨 refactor layout in InboxListScreen for improved readability and UI …
Whyjsee Mar 12, 2026
e94cc3c
🎨 enhance image loading styles and improve error handling in webview …
Whyjsee Mar 13, 2026
4119a74
🎨 add localization for minimize button and enhance layout in OutlineD…
Whyjsee Mar 13, 2026
9d7667b
🎨 enhance animation for OutlineDialog and FloatingActionBar for impro…
Whyjsee Mar 13, 2026
84b1028
🎨 add confirmation dialog for bookmark deletion with localization sup…
Whyjsee Mar 13, 2026
36f33a6
🎨 enhance animation transitions in OutlineDialog for improved user ex…
Whyjsee Mar 16, 2026
fa7ed12
🎨 improve transition handling and visibility logic in OutlineDialog f…
Whyjsee Mar 16, 2026
e4d8733
🎨 refactor OutlineDialog and add loading animations for improved UI r…
Whyjsee Mar 16, 2026
c59ed46
Merge branch 'main' into feature/ui-improve
Whyjsee Mar 16, 2026
e4561d7
🎨 add scroll position management to OutlineDialog for improved user e…
Whyjsee Mar 16, 2026
74f6a86
🎨 implement scroll position persistence for OutlineDialog to enhance …
Whyjsee Mar 16, 2026
01ee4f6
🎨 refactor bookmark deletion flow and enhance loading animations for …
Whyjsee Mar 16, 2026
b34a015
🎨 enhance scroll position management in OutlineDialog with debounce f…
Whyjsee Mar 16, 2026
6c92133
🎨 rename scroll position references to read position in LocalBookmark…
Whyjsee Mar 16, 2026
4ffdac7
🎨 simplify loading animation comments and remove unnecessary code for…
Whyjsee Mar 17, 2026
99b9906
🎨 refactor animation handling in OutlineDialog for improved clarity a…
Whyjsee Mar 17, 2026
4af53d5
🎨 simplify image loading indicator comments for improved clarity
Whyjsee Mar 17, 2026
73b3957
🎨 add toolbar visibility animation to OutlineDialog for improved UX
Whyjsee Mar 17, 2026
c3b9da1
🎨 improve Markdown content rendering in ExpandedOutlineDialog to prev…
Whyjsee Mar 17, 2026
d4fb280
🎨 enhance clickable interaction in OverviewView with custom ripple ef…
Whyjsee Mar 17, 2026
08399ba
🎨 adjust top spacing in LoginScreen for responsive design
Whyjsee Mar 17, 2026
5fb7d53
🎨 add showCollapsed method to OutlineDelegate and invoke it based on …
Whyjsee Mar 17, 2026
1c61571
feat: version update
Whyjsee Mar 17, 2026
c629fd4
feat: update version
Whyjsee Mar 17, 2026
3c93c95
🎨 disable pretty print in HTML processing for improved content handling
Whyjsee Mar 18, 2026
28c0b6c
feat: implement bookmark deletion with animation and navigation back
Whyjsee Mar 18, 2026
60b1b35
feat: implement dismissable item for bookmark deletion animation
Whyjsee Mar 18, 2026
6a2c292
Merge branch 'main' into feature/cell-deletion
Whyjsee Mar 19, 2026
8aeaa48
🎨 add localized strings for toolbar actions in detail view for improv…
Whyjsee Mar 19, 2026
d4daf56
chore: update appVersionCode to 714 for upcoming release
Whyjsee Mar 19, 2026
a63eaee
refactor: remove unnecessary @Volatile annotation from pendingDeleteId
Whyjsee Mar 20, 2026
732a930
Merge branch 'main' into feature/cell-deletion
Whyjsee Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.slax.reader.const.component

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.delay

/**
* 带消失动画的容器:先变暗,再收缩淡出。
*
* @param isDismissed 触发消失动画
* @param durationMs 收缩+淡出阶段时长(毫秒)
* @param onDismissed 动画完全结束后的回调
* @param content 被包裹的内容
*/
@Composable
fun DismissableItem(
isDismissed: Boolean,
durationMs: Int = 600,
onDismissed: () -> Unit = {},
content: @Composable () -> Unit,
) {
var itemVisible by remember { mutableStateOf(true) }
val overlayAlpha = remember { Animatable(0f) }

LaunchedEffect(isDismissed) {
if (isDismissed && itemVisible) {
delay(50)
overlayAlpha.animateTo(0.08f, tween(durationMs / 2))
delay(100)
itemVisible = false
}
}

AnimatedVisibility(
visible = itemVisible,
exit = shrinkVertically(
animationSpec = tween(durationMs + 100, easing = FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(durationMs)),
) {
Box {
content()
if (overlayAlpha.value > 0f) {
Box(
modifier = Modifier
.matchParentSize()
.background(Color.Black.copy(alpha = overlayAlpha.value))
)
}
}
}

if (!itemVisible) {
LaunchedEffect(Unit) {
delay(durationMs + 150L)
onDismissed()
}
}
}
2 changes: 2 additions & 0 deletions composeApp/src/commonMain/kotlin/com/slax/reader/di/di.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.slax.reader.data.file.FileManager
import com.slax.reader.data.network.ApiService
import com.slax.reader.data.preferences.preferencesPlatformModule
import com.slax.reader.domain.auth.AuthDomain
import com.slax.reader.domain.bookmark.BookmarkActionBus
import com.slax.reader.domain.coordinator.CoordinatorDomain
import com.slax.reader.domain.image.ImageDownloadManager
import com.slax.reader.domain.sync.BackgroundDomain
Expand Down Expand Up @@ -75,6 +76,7 @@ val domainModule = module {
single { BackgroundDomain(get(), get(), get(), get(), get(), get()) }
single { CoordinatorDomain(get(), get(), get()) }
single { ImageDownloadManager(get(), get()) }
single { BookmarkActionBus() }
}

val appModule = module {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.slax.reader.domain.bookmark

/**
* 跨页面书签操作事件总线(单例)。
* 详情页发出删除事件后,由列表页在可见时消费并播放动画。
*/
class BookmarkActionBus {
// 待消费的删除 ID,保证即使列表页 composable 不在组合树中也不会丢失事件
var pendingDeleteId: String? = null
private set

fun emitDelete(bookmarkId: String) {
pendingDeleteId = bookmarkId
}

fun consumePendingDelete(): String? {
val id = pendingDeleteId
pendingDeleteId = null
return id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.slax.reader.data.network.ApiService
import com.slax.reader.data.preferences.AppPreferences
import com.slax.reader.data.preferences.ContinueReadingBookmark
import com.slax.reader.domain.sync.BackgroundDomain
import com.slax.reader.domain.bookmark.BookmarkActionBus
import com.slax.reader.ui.bookmark.states.BookmarkDelegate
import com.slax.reader.ui.bookmark.states.BookmarkOverlay
import com.slax.reader.ui.bookmark.states.CommentDelegate
Expand Down Expand Up @@ -51,7 +52,6 @@ sealed interface BookmarkDetailEffect {
data object NavigateBack : BookmarkDetailEffect
data object NavigateToSubscription : BookmarkDetailEffect
data class NavigateToFeedback(val params: FeedbackPageParams) : BookmarkDetailEffect

data class ScrollToAnchor(val anchor: String) : BookmarkDetailEffect
}

Expand All @@ -70,6 +70,7 @@ class BookmarkDetailViewModel(
private val apiService: ApiService,
private val appPreferences: AppPreferences,
private val database: PowerSyncDatabase,
private val bookmarkActionBus: BookmarkActionBus,
) : ViewModel() {

companion object {
Expand Down Expand Up @@ -197,17 +198,13 @@ class BookmarkDetailViewModel(

fun confirmDeleteBookmark() {
viewModelScope.launch {
runCatching { bookmarkDelegate.deleteBookmark() }
.onSuccess {
bookmarkEvent.action("delete").send()
_deleteConfirmVisible.value = false
overlayDelegate.dismissOverlay(BookmarkOverlay.Toolbar)
requestNavigateBack()
}
.onFailure {
bookmarkEvent.action("delete_failed").send()
_deleteConfirmVisible.value = false
}
val id = _bookmarkId.value ?: return@launch
_deleteConfirmVisible.value = false
overlayDelegate.dismissOverlay(BookmarkOverlay.Toolbar)
// 只通知列表页播放删除动画,实际删除由列表页的 ViewModel 在动画结束后执行
bookmarkActionBus.emitDelete(id)
bookmarkEvent.action("delete").send()
requestNavigateBack()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class OutlineDelegate(
val id = currentBookmarkId ?: return
val position = currentScrollPosition
if (position < 0) return

scope.launch {
withContext(NonCancellable + Dispatchers.IO) {
localBookmarkDao.updateLocalBookmarkOutlineScrollPosition(id, position)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,29 @@ import com.slax.reader.data.database.dao.LocalBookmarkDao
import com.slax.reader.data.database.dao.UserDao
import com.slax.reader.data.database.model.InboxListBookmarkItem
import com.slax.reader.domain.coordinator.CoordinatorDomain
import com.slax.reader.domain.bookmark.BookmarkActionBus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class InboxListViewModel(
private val userDao: UserDao,
private val bookmarkDao: BookmarkDao,
private val localBookmarkDao: LocalBookmarkDao,
private val coordinatorDomain: CoordinatorDomain
private val coordinatorDomain: CoordinatorDomain,
private val bookmarkActionBus: BookmarkActionBus,
) : ViewModel() {
val userInfo = userDao.watchUserInfo()
val syncState = coordinatorDomain.syncState
Expand All @@ -45,13 +52,60 @@ class InboxListViewModel(
private val _scrollToTopEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val scrollToTopEvent: SharedFlow<Unit> = _scrollToTopEvent.asSharedFlow()

private val _pendingDeleteId = MutableStateFlow<String?>(null)
val pendingDeleteId: StateFlow<String?> = _pendingDeleteId.asStateFlow()

private val _processingUrlEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
val processingUrlEvent: SharedFlow<String> = _processingUrlEvent.asSharedFlow()

// 兜底删除定时器,确保动画被中断时也能执行删除
private var pendingDeleteJob: Job? = null

fun scrollToTop() {
_scrollToTopEvent.tryEmit(Unit)
}

/**
* 当列表页 composable 进入组合树时调用,
* 从 BookmarkActionBus 消费待删除 ID 并触发动画。
* 确保动画只在列表页可见时才播放。
*/
fun activatePendingDelete() {
val id = bookmarkActionBus.consumePendingDelete() ?: return
_pendingDeleteId.value = id
startDeleteFallback(id)
}

fun onDeleteAnimationFinished() {
val id = _pendingDeleteId.value
_pendingDeleteId.value = null
pendingDeleteJob?.cancel()
if (id != null) {
viewModelScope.launch { commitDelete(id) }
}
}

private suspend fun commitDelete(bookmarkId: String) {
withContext(Dispatchers.IO) { bookmarkDao.deleteBookmark(bookmarkId) }
}

private fun startDeleteFallback(id: String) {
pendingDeleteJob?.cancel()
pendingDeleteJob = viewModelScope.launch {
delay(3000L)
commitDelete(id)
}
}

/**
* 列表页直接触发删除(长按菜单),
* 无需经过 ActionBus 缓冲,直接设置 pendingDeleteId 播放动画。
*/
fun requestDeleteBookmark(bookmarkId: String) {
_pendingDeleteId.value = bookmarkId
startDeleteFallback(bookmarkId)
}

fun emitProcessingUrl(url: String) {
_processingUrlEvent.tryEmit(url)
}
Expand All @@ -70,11 +124,7 @@ class InboxListViewModel(
bookmarkDao.updateBookmarkArchive(bookmarkId, if (isArchive) 1 else 0)
}

suspend fun deleteBookmark(bookmarkId: String) = withContext(Dispatchers.IO) {
bookmarkDao.deleteBookmark(bookmarkId)
}

suspend fun addLinkBookmark(url: String) = withContext(Dispatchers.IO) {
return@withContext bookmarkDao.createBookmark(url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ fun BookmarkItemRow(
scope.launch {
menuTriggerSource = MenuTriggerSource.NONE
isLongPressed = false
viewModel.deleteBookmark(bookmark.id)
viewModel.requestDeleteBookmark(bookmark.id)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.slax.reader.const.component.DismissableItem
import com.slax.reader.data.database.model.InboxListBookmarkItem
import com.slax.reader.ui.inbox.InboxListViewModel
import com.slax.reader.utils.i18n
Expand All @@ -33,6 +34,7 @@ fun ArticleList(
println("[watch][UI] recomposition ArticleList")

val bookmarks by viewModel.bookmarks.collectAsState()
val pendingDeleteId by viewModel.pendingDeleteId.collectAsState()

val lazyListState = rememberLazyListState()
val dividerLine: @Composable () -> Unit = remember {
Expand All @@ -45,6 +47,11 @@ fun ArticleList(
}
}

// 列表页进入组合树时,消费详情页传来的待删除 ID 并触发动画
LaunchedEffect(Unit) {
viewModel.activatePendingDelete()
}

if (bookmarks.isEmpty()) {
val hasSynced by viewModel.hasSynced.collectAsState()
Box(
Expand All @@ -68,15 +75,21 @@ fun ArticleList(
items = bookmarks,
key = { _, bookmark -> bookmark.id },
contentType = { _, _ -> "bookmark" }
) { index, bookmark ->
BookmarkItemRow(
navCtrl = navCtrl,
viewModel = viewModel,
bookmark = bookmark,
onEditTitle = onEditTitle
)

dividerLine()
) { _, bookmark ->
DismissableItem(
isDismissed = bookmark.id == pendingDeleteId,
onDismissed = { viewModel.onDeleteAnimationFinished() },
) {
Column {
BookmarkItemRow(
navCtrl = navCtrl,
viewModel = viewModel,
bookmark = bookmark,
onEditTitle = onEditTitle
)
dividerLine()
}
}
}

item {
Expand Down