Skip to content

Commit 34b4c41

Browse files
NateEatonclaude
andauthored
feat: deterministic sync progress indicator + related housekeeping (#160)
* feat: deterministic sync progress indicator + related housekeeping Bookmark list (initial sync): - Add BookmarkSyncProgress sealed interface (Idle / Running) to BookmarkRepository; emit Running(page, totalPages) after each page in performFullSync(), reset to Idle in finally block. - Fix BookmarkRepositoryImpl binding in AppModule: add @singleton so the Worker and ViewModel share the same instance and progress state is visible across both. - Expose syncFraction: StateFlow<Float?> from BookmarkListViewModel, mapped from syncProgress. - Replace pull-to-refresh spinner on initial load with a deterministic LinearProgressIndicator below the TopAppBar; PullToRefreshBox spinner is now only shown for user-initiated refreshes. - Auto-scroll list to top when initial sync completes via snapshotFlow on isInitialLoading → scrollToTopTrigger. Highlights (initial sync / pull-to-refresh): - Add indeterminate LinearProgressIndicator below TopAppBar for auto-syncs (guarded by isRefreshing && !isUserRefreshing && !isInitialLocalLoad). - Add isUserRefreshing tracking to HighlightsViewModel: set on retry(), reset via init observer when sync state leaves Running. Wire into HighlightsUiState and PullToRefreshBox so the circular spinner only shows for user-initiated pulls. - Remove inline CircularProgressIndicator from the empty-state body (replaced by top-bar LinearProgressIndicator). Logging: - Revert HttpLoggingInterceptor from BODY back to BASIC; full response bodies were excessive for large libraries and impacted app load time. Housekeeping: - Update CHANGELOG.md with entries for 0.11.0 through 0.13.0. - Update docs/WORKFLOW.md: add CHANGELOG.md update step to release prep. - Remove _notes/reader-top-bar-scroll-behavior-spec.md from git tracking (_notes/ is local-only per .gitignore). - Add docs/specs/deterministic-sync-progress-indicator-spec.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: pin JDK to 17.0.14 and capture JVM crash logs Intermittent C2 JIT crashes (Node::uncast SIGSEGV) were occurring on the verify job. Pin the Temurin patch version for deterministic builds and upload hs_err/replay/core files as artifacts on failure to enable diagnosis if the crash recurs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Fix sync progress review issues and pin CI JDK - Show bookmark sync progress in the list UI only during initial loading, keeping background and user-triggered periodic full syncs visually quiet. - Reset highlights user-refresh tracking when a retry is skipped or fails, preventing later automatic refreshes from being mislabeled as user refreshes. - Add regression coverage for skipped highlights retry followed by automatic refresh. - Stub `BookmarkRepository.syncProgress` in `BookmarkListViewModelTest` to fix MockK unit test failures. - Pin the quality checks workflow to Temurin JDK 17.0.13 to avoid the GitHub Actions JVM crash seen with 17.0.14. Verification: - `./gradlew :app:assembleDebugAll` - `./gradlew :app:testDebugUnitTestAll` - `./gradlew :app:lintDebugAll` --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3fc92de commit 34b4c41

15 files changed

Lines changed: 335 additions & 94 deletions

.github/workflows/checks.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
uses: actions/setup-java@v4
3838
with:
3939
distribution: temurin
40-
java-version: 17
40+
java-version: '17.0.13'
4141

4242
- name: Setup Gradle
4343
uses: gradle/actions/setup-gradle@v4
@@ -56,3 +56,15 @@ jobs:
5656

5757
- name: Run Tests
5858
run: ./gradlew :app:testDebugUnitTestAll
59+
60+
- name: Upload JVM crash logs
61+
if: failure()
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: jvm-crash-logs
65+
path: |
66+
**/hs_err_pid*.log
67+
**/replay_pid*.log
68+
**/core.*
69+
if-no-files-found: ignore
70+
retention-days: 14

CHANGELOG.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.13.0] - 2026-05-18
11+
12+
### Added
13+
14+
- Highlights: select and save text highlights while reading articles, with support for adding notes to each highlight
15+
- Global highlights list in the navigation drawer: browse, search, filter, and sort all highlights across your bookmarks
16+
- Swipe actions on bookmark cards for quick access to favorite, archive, and delete
17+
18+
### Changed
19+
20+
- Reader now uses native WebView scrolling for smoother, more reliable article navigation
21+
- Picture bookmark reading view layout updated to match Readeck's presentation
22+
23+
### Fixed
24+
25+
- Foreground service crash on Android 14+ devices
26+
- Reader top bar overlay behavior corrected
27+
- Sync cancellation and resilience improvements
28+
29+
## [0.12.6] - 2026-05-05
30+
31+
### Fixed
32+
33+
- Resolved a critical OutOfMemoryError that caused crashes during offline content synchronization
34+
35+
### Changed
36+
37+
- Cleaned up obsolete code and reduced app footprint
38+
39+
## [0.12.0] - 2026-04-11
40+
41+
### Added
42+
43+
- In-page anchor link support in reader content: table-of-contents and fragment links now navigate correctly within articles
44+
- Long-press context menu for in-page links to open or copy anchor targets
45+
- Content download status icon on reading-view bookmark cards
46+
- Reading progress icon in Compact list view
47+
48+
### Changed
49+
50+
- Sync architecture updated to multipart sync for improved reliability and consistency when refreshing bookmark metadata and content
51+
- Sync Settings revised with automatic content sync for offline reading using volume, item-count, or date-range policies
52+
53+
### Fixed
54+
55+
- Reader text reflow regression: resolved cases where article text could disappear after layout/font reflow updates
56+
- Server URL validation now allows http:// endpoints (in addition to https://) for self-hosted/local Readeck setups
57+
- Offline status indicator now tracks network transitions more accurately and avoids incorrect offline icon states
58+
59+
## [0.11.1] - 2026-03-19
60+
1061
### Added
1162

63+
- Special thanks to Stefan (@Alanon202) in About screen and README for app functionality feedback and testing support
64+
1265
### Changed
1366

67+
- Background sync indicator removed for cleaner UI
68+
- Filter chip behavior: dismissing synthetic chips restores preset defaults, literal chip removal remains unchanged
69+
1470
### Fixed
1571

72+
- Server error flag propagation: bookmarks with server errors now correctly appear in "With errors" filter after refresh/create operations
73+
- Text autosizing in reader: fixed 8-12% text size increase when switching from Medium to Wide reader width
74+
- Filter UI: synthetic filter chips now appear when preset constraints are broadened (e.g., "Is archived: N/A", "Is favorite: N/A", "Type: Any")
75+
- Sync performance: reduced blocking spinner on app open by showing cached bookmarks immediately during background sync
76+
- Delete operation: fixed race condition with rapid successive delete actions
77+
- Layout stability: eliminated theme switching reflow and fixed layout shift in sync indicator
78+
- Video controls: improved fullscreen discoverability and auto-rotation behavior
79+
- Missing translations: added localized "Copy to clipboard" text for all languages
80+
81+
## [0.11.0] - 2026-03-14
82+
83+
### Added
84+
85+
- Image gallery lightbox in reading view: tap any article image to open a full-screen gallery with swipe navigation, pinch-to-zoom, double-tap to zoom, and a thumbnail strip
86+
- Long-press context menus for images and links in reader view and bookmark list (copy, download, share, open in browser)
87+
- Highlights and annotations: view, create, and edit Readeck highlights directly in the reading view
88+
- "Keep screen on while reading" toggle in Settings → User Interface
89+
- Reader appearance settings: curated themes, font size, line spacing, content width, and fullscreen mode
90+
- Fullscreen reading mode: hides top bar while reading; swipe up to reveal controls
91+
- Typography and Find in Page now available for Video and Picture bookmark types
92+
- About screen shows app and server info in collapsible cards (version, build, server name, URL)
93+
- 15-minute option added to auto-sync schedule
94+
95+
### Changed
96+
97+
- Favorite and Archive actions moved from top bar to overflow menu and inline buttons at end of article content
98+
- Long-press context menus replaced with centered dialog popups showing a preview header
99+
- "View original" renamed to "View web page" throughout
100+
- Menu items and filter labels now use sentence case
101+
- Bookmark deletion: card stays visible but greyed-out until snackbar is dismissed or undo is pressed
102+
- Delta sync re-enabled for Readeck 0.22+; deleted bookmarks now detected immediately on pull-to-refresh
103+
- Navigation drawer and settings screen typography refined for better visual hierarchy
104+
- User guide gains a "Contents" button for one-tap navigation back to the table of contents
105+
16106
## [0.10.0] - 2026-02-26
17107

18108
### Added

_notes/reader-top-bar-scroll-behavior-spec.md

Lines changed: 0 additions & 76 deletions
This file was deleted.

app/src/main/java/com/mydeck/app/AppModule.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ abstract class AppModule {
3232
abstract fun bindHighlightsRepository(highlightsRepositoryImpl: com.mydeck.app.domain.HighlightsRepositoryImpl): com.mydeck.app.domain.HighlightsRepository
3333

3434
@Binds
35+
@Singleton
3536
abstract fun bindBookmarkRepository(bookmarkRepositoryImpl: BookmarkRepositoryImpl): BookmarkRepository
3637

3738
@Binds

app/src/main/java/com/mydeck/app/domain/BookmarkRepository.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.mydeck.app.domain.model.BookmarkListItem
66
import com.mydeck.app.domain.model.BookmarkMetadataUpdate
77
import com.mydeck.app.domain.model.ProgressFilter
88
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.StateFlow
910

1011
interface BookmarkRepository {
1112
fun observeBookmarks(
@@ -40,6 +41,8 @@ interface BookmarkRepository {
4041
suspend fun updateBookmark(bookmarkId: String, isFavorite: Boolean?, isArchived: Boolean?, isRead: Boolean?): UpdateResult
4142
suspend fun updateReadProgress(bookmarkId: String, progress: Int): UpdateResult
4243
suspend fun updateLabels(bookmarkId: String, labels: List<String>): UpdateResult
44+
val syncProgress: StateFlow<BookmarkSyncProgress>
45+
4346
suspend fun performFullSync(): SyncResult
4447
suspend fun performDeltaSync(since: kotlinx.datetime.Instant?): SyncResult
4548
suspend fun syncPendingActions(): UpdateResult
@@ -83,6 +86,11 @@ interface BookmarkRepository {
8386
suspend fun refreshBookmarkMetadata(bookmarkId: String)
8487
suspend fun fetchExtractionLog(bookmarkId: String): ExtractionLogResult
8588

89+
sealed interface BookmarkSyncProgress {
90+
data object Idle : BookmarkSyncProgress
91+
data class Running(val page: Int, val totalPages: Int) : BookmarkSyncProgress
92+
}
93+
8694
sealed class ExtractionLogResult {
8795
data class Success(val text: String) : ExtractionLogResult()
8896
data class HttpError(val code: Int) : ExtractionLogResult()

app/src/main/java/com/mydeck/app/domain/BookmarkRepositoryImpl.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ import kotlinx.coroutines.CoroutineScope
3737
import kotlinx.coroutines.Dispatchers
3838
import kotlinx.coroutines.delay
3939
import kotlinx.coroutines.flow.Flow
40+
import kotlinx.coroutines.flow.MutableStateFlow
41+
import kotlinx.coroutines.flow.StateFlow
42+
import kotlinx.coroutines.flow.asStateFlow
4043
import kotlinx.coroutines.flow.map
4144
import kotlinx.coroutines.launch
4245
import kotlinx.coroutines.NonCancellable
@@ -66,6 +69,10 @@ class BookmarkRepositoryImpl @Inject constructor(
6669
@IoDispatcher
6770
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
6871
) : BookmarkRepository {
72+
73+
private val _syncProgress = MutableStateFlow<BookmarkRepository.BookmarkSyncProgress>(BookmarkRepository.BookmarkSyncProgress.Idle)
74+
override val syncProgress: StateFlow<BookmarkRepository.BookmarkSyncProgress> = _syncProgress.asStateFlow()
75+
6976
override fun observeBookmarks(
7077
type: Bookmark.Type?,
7178
unread: Boolean?,
@@ -755,6 +762,8 @@ class BookmarkRepositoryImpl @Inject constructor(
755762
totalInserted += bookmarks.size
756763
}
757764

765+
_syncProgress.value = BookmarkRepository.BookmarkSyncProgress.Running(currentPage, totalPages)
766+
758767
if (currentPage < totalPages) {
759768
offset += pageSize
760769
} else {
@@ -816,6 +825,7 @@ class BookmarkRepositoryImpl @Inject constructor(
816825
Timber.e(e, "Full sync failed")
817826
BookmarkRepository.SyncResult.NetworkError(errorMessage = "Network error during full sync", ex = e)
818827
} finally {
828+
_syncProgress.value = BookmarkRepository.BookmarkSyncProgress.Idle
819829
// NonCancellable ensures cleanup runs even when the coroutine is cancelled.
820830
withContext(NonCancellable) {
821831
try {

app/src/main/java/com/mydeck/app/io/rest/NetworkModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ object NetworkModule {
5151
Timber.tag("OkHttp").d(message)
5252
}
5353
val loggingInterceptor = HttpLoggingInterceptor(timberLogger).apply {
54-
level = HttpLoggingInterceptor.Level.BODY
54+
level = HttpLoggingInterceptor.Level.BASIC
5555
redactHeader("Authorization")
5656
redactHeader("Cookie")
5757
redactHeader("Set-Cookie")

app/src/main/java/com/mydeck/app/ui/highlights/HighlightsScreen.kt

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ fun HighlightsContent(
214214
if (uiState.cachePartial && uiState.groups.isNotEmpty()) {
215215
HighlightsPartialCacheBanner()
216216
}
217+
if (uiState.isRefreshing && !uiState.isUserRefreshing && !uiState.isInitialLocalLoad) {
218+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
219+
}
217220
}
218221
}
219222
) { paddingValues ->
@@ -246,16 +249,6 @@ fun HighlightsContent(
246249
MaterialTheme.colorScheme.onSurfaceVariant
247250
}
248251
)
249-
if (uiState.isRefreshing) {
250-
Spacer(Modifier.height(12.dp))
251-
CircularProgressIndicator(modifier = Modifier.size(24.dp))
252-
Spacer(Modifier.height(8.dp))
253-
Text(
254-
text = stringResource(R.string.highlights_refreshing),
255-
style = MaterialTheme.typography.bodyMedium,
256-
color = MaterialTheme.colorScheme.onSurfaceVariant
257-
)
258-
}
259252
if (uiState.refreshFailed) {
260253
Spacer(Modifier.height(8.dp))
261254
Button(onClick = onRetry) {
@@ -274,7 +267,7 @@ fun HighlightsContent(
274267
} else {
275268
val pullToRefreshState = rememberPullToRefreshState()
276269
PullToRefreshBox(
277-
isRefreshing = uiState.isRefreshing,
270+
isRefreshing = uiState.isUserRefreshing,
278271
onRefresh = onRetry,
279272
state = pullToRefreshState,
280273
modifier = Modifier.fillMaxSize()

0 commit comments

Comments
 (0)