Skip to content

Commit 26f9c19

Browse files
committed
Prepare 1.0.0 release
1 parent 5501bd5 commit 26f9c19

13 files changed

Lines changed: 211 additions & 64 deletions

File tree

.github/workflows/build.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ jobs:
6262
- name: Assemble debug APK
6363
run: ./gradlew --no-daemon assembleDebug
6464

65+
- name: Build release artifacts
66+
run: ./gradlew --no-daemon :app:assembleRelease :companion:assembleRelease :app:bundleRelease :companion:bundleRelease
67+
6568
- name: Upload debug APK (watch)
6669
uses: actions/upload-artifact@v4
6770
with:
@@ -77,3 +80,35 @@ jobs:
7780
path: companion/build/outputs/apk/debug/companion-debug.apk
7881
if-no-files-found: error
7982
retention-days: 3
83+
84+
- name: Upload release APK (watch)
85+
uses: actions/upload-artifact@v4
86+
with:
87+
name: app-release-apk
88+
path: app/build/outputs/apk/release/app-release.apk
89+
if-no-files-found: error
90+
retention-days: 3
91+
92+
- name: Upload release APK (companion)
93+
uses: actions/upload-artifact@v4
94+
with:
95+
name: companion-release-apk
96+
path: companion/build/outputs/apk/release/companion-release.apk
97+
if-no-files-found: error
98+
retention-days: 3
99+
100+
- name: Upload release AAB (watch)
101+
uses: actions/upload-artifact@v4
102+
with:
103+
name: app-release-aab
104+
path: app/build/outputs/bundle/release/app-release.aab
105+
if-no-files-found: error
106+
retention-days: 3
107+
108+
- name: Upload release AAB (companion)
109+
uses: actions/upload-artifact@v4
110+
with:
111+
name: companion-release-aab
112+
path: companion/build/outputs/bundle/release/companion-release.aab
113+
if-no-files-found: error
114+
retention-days: 3

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ All notable changes to wBooks are documented in this file.
44

55
## [Unreleased]
66

7-
## [0.9.0] - 2026-05-24
7+
## [1.0.0] - 2026-05-24
88

99
- Reduced watch reader memory pressure: embedded images now decode at display size, and stale parsed-document cache writes are cancelled when switching or closing books.
1010
- Improved Time left behavior: normal/sentence scrolling now feeds the pace estimate more reliably, and speed-reading mode gets immediate WPM-based ETA.
1111
- Polished watch Project Gutenberg search: keyboard search submits directly, voice/text controls match, and downloaded entries look like existing library books.
12+
- Watch Project Gutenberg downloads now show inline progress, flip to Added when complete, and remain tracked after renamed or moved.
1213
- Book lists now show file size beside format, and bundled seed books use Gutenberg-compatible titles on fresh installs.
14+
- Web interface and Utility now show watch reading statistics from the same watch-authoritative source.
1315
- Web and Utility storage summaries now show library usage and free space only, not total device storage.
1416
- Utility Root layout is adjustable by dragging and leaves more space when folders exist.
17+
- Utility watch requests now time out instead of hanging forever, and any lost watch connection opens the reconnect screen, including mid-session Gutenberg adds.
1518
- Web same-folder drag-and-drop now updates order directly instead of treating the drop as a move.
1619
- Hardened Wear/web transfer cleanup and reduced temporary disk use during web uploads.
1720
- GitHub Actions artifact/caching retention was tightened to reduce repository Actions storage usage.
21+
- CI now builds signed release APKs and AABs for GitHub/direct install and Play Console testing.
22+
23+
## [0.9.0] - 2026-05-24
24+
25+
- Release-candidate build used for final watch, Utility, webserver, Gutenberg, storage, and Actions cleanup testing.
1826

1927
## [0.8.0] — 2026-05-24
2028

app/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ android {
7878
applicationId = requireLocalProperty("wbooks.applicationId")
7979
minSdk = 30
8080
targetSdk = 35
81-
versionCode = 12
82-
versionName = "0.9.0"
81+
versionCode = 13
82+
versionName = "1.0.0"
8383

8484
manifestPlaceholders["sentryDsn"] = localProperty("sentry.dsn")
8585
}
@@ -133,7 +133,8 @@ android {
133133
// Only enable Sentry symbol/source upload when an auth token is available.
134134
// CI builds debug only and has no token; without this gate the Sentry Gradle
135135
// plugin tries to bundle+upload source context during assembleDebug and fails.
136-
val hasSentryAuthToken = localProperty("sentry.auth.token").isNotBlank()
136+
val skipSentryUpload = providers.gradleProperty("wbooks.skipSentryUpload").orNull == "true"
137+
val hasSentryAuthToken = localProperty("sentry.auth.token").isNotBlank() && !skipSentryUpload
137138

138139
sentry {
139140
org.set("fredapps")

app/src/main/kotlin/com/fredapp/wbooks/data/changelog/Changelog.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,26 @@ data class ChangelogEntry(
1212
*/
1313
val CHANGELOG: List<ChangelogEntry> = listOf(
1414
ChangelogEntry(
15-
version = "0.9.0",
15+
version = "1.0.0",
1616
date = "2026-05-24",
1717
notes = listOf(
1818
"Reader memory pressure reduced: embedded images are decoded at watch-display size, and stale parsed-document cache writes are cancelled when switching or closing books.",
1919
"Time left estimates now learn from sustained normal/sentence scrolling and speed-reading mode shows an immediate WPM-based estimate.",
2020
"The web interface now includes read-only reading statistics from the watch.",
21-
"Watch Project Gutenberg search controls were tightened: keyboard search submits directly, voice/text controls match visually, and downloaded entries look like existing library books.",
21+
"Watch Project Gutenberg search controls were tightened: keyboard search submits directly, voice/text controls match visually, downloads show inline progress, and completed entries stay marked as Added after rename or move.",
2222
"Book lists now show file size beside format, and bundled seed books use Gutenberg-compatible titles on fresh installs.",
2323
"Web and Utility library storage summaries now show only library usage and free space, not the device's total storage volume.",
2424
"Web same-folder drag-and-drop now updates book order directly instead of treating the drop as a move.",
2525
"Wear and web uploads clean up interrupted transfers more aggressively and avoid extra temporary disk use where possible.",
2626
),
2727
),
28+
ChangelogEntry(
29+
version = "0.9.0",
30+
date = "2026-05-24",
31+
notes = listOf(
32+
"Release-candidate build used for final watch, Utility, webserver, Gutenberg, storage, and Actions cleanup testing.",
33+
),
34+
),
2835
ChangelogEntry(
2936
version = "0.8.0",
3037
date = "2026-05-24",

companion/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ android {
7272
applicationId = requireLocalProperty("wbooks.applicationId")
7373
minSdk = 24
7474
targetSdk = 35
75-
versionCode = 12
76-
versionName = "0.9.0"
75+
versionCode = 13
76+
versionName = "1.0.0"
7777

7878
manifestPlaceholders["sentryDsn"] = localProperty("sentry.dsn")
7979
}
@@ -117,7 +117,8 @@ android {
117117
}
118118
}
119119

120-
val hasSentryAuthToken = localProperty("sentry.auth.token").isNotBlank()
120+
val skipSentryUpload = providers.gradleProperty("wbooks.skipSentryUpload").orNull == "true"
121+
val hasSentryAuthToken = localProperty("sentry.auth.token").isNotBlank() && !skipSentryUpload
121122

122123
sentry {
123124
org.set("fredapps")

companion/proguard-rules.pro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
-keep class org.jsoup.** { *; }
55
-dontwarn org.jsoup.**
66

7+
# --- PDFBox treats JPEG-2000 support as optional; the converter still works
8+
# for normal text PDFs when the JP2 decoder artifact is absent.
9+
-dontwarn com.gemalto.jp2.JP2Decoder
10+
711
# --- Sentry auto-init reflection (see app/proguard-rules.pro for context).
812
-keep class io.sentry.android.core.SentryAndroidOptions { *; }
913
-keep class io.sentry.android.core.SentryInitProvider { *; }

companion/src/main/kotlin/com/fredapp/wbooksutil/CompanionChangelog.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ data class CompanionChangelogEntry(
1414
object CompanionChangelog {
1515
val ENTRIES: List<CompanionChangelogEntry> = listOf(
1616
CompanionChangelogEntry(
17-
version = "0.9.0",
17+
version = "1.0.0",
1818
date = "2026-05-24",
1919
notes = listOf(
20+
"Utility watch requests now time out instead of hanging forever, and any lost watch connection opens the Reconnect screen, including mid-session Gutenberg adds.",
2021
"Utility root layout is easier to manage: Root sits lower when folders exist and its position can be adjusted by dragging.",
2122
"Reading stats keep refreshing from the watch while the Utility stats screen is open.",
2223
"Storage summary now shows library usage and free space only, matching the watch web interface.",
@@ -25,6 +26,13 @@ object CompanionChangelog {
2526
"Shared upload and watch-sync behavior benefits from the watch-side transfer cleanup in this release.",
2627
),
2728
),
29+
CompanionChangelogEntry(
30+
version = "0.9.0",
31+
date = "2026-05-24",
32+
notes = listOf(
33+
"Release-candidate build used for final Utility, library sync, settings, stats, and Project Gutenberg testing.",
34+
),
35+
),
2836
CompanionChangelogEntry(
2937
version = "0.8.0",
3038
date = "2026-05-24",

companion/src/main/kotlin/com/fredapp/wbooksutil/GutenbergScreen.kt

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.*
1414
import androidx.compose.runtime.saveable.rememberSaveable
1515
import androidx.compose.ui.Alignment
1616
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.res.stringResource
1718
import androidx.compose.ui.text.font.FontWeight
1819
import androidx.compose.ui.text.style.TextOverflow
1920
import androidx.compose.ui.unit.dp
@@ -51,44 +52,19 @@ fun GutenbergScreen(vm: GutenbergViewModel, onBack: () -> Unit) {
5152
snackbarHost = { GutenbergSnackbarHost(state, snackbarState, onCancelDownload = vm::cancelDownload) },
5253
) { padding ->
5354
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
54-
SearchBar(
55-
value = state.query,
56-
onValueChange = vm::onQueryChange,
57-
onSubmit = vm::submitSearch,
58-
)
5955
when {
60-
state.loading && state.visibleBooks.isEmpty() -> CenteredProgress()
61-
state.showingSearch && state.searchResults.isEmpty() -> CenteredText("No results.")
62-
state.showingSearch -> ResultsList(
63-
results = state.searchResults,
64-
downloadingId = state.downloadingId,
65-
loadingMore = state.loadingMore,
66-
hasMore = state.searchHasMore,
67-
onLoadMore = { vm.loadMore(GutenbergListTarget.SEARCH) },
68-
isPresentOnDevice = vm::isPresentOnDevice,
69-
onAdd = vm::sendToWatch,
70-
)
71-
state.popularBooks.isEmpty() && state.recentReleases.isEmpty() -> CenteredText("No books.")
72-
else -> HomeSections(
73-
popularBooks = state.popularBooks,
74-
recentReleases = state.recentReleases,
75-
selectedSection = homeSection,
76-
onSectionChange = { homeSection = it },
77-
downloadingId = state.downloadingId,
78-
loadingMore = state.loadingMore,
79-
popularHasMore = state.popularHasMore,
80-
recentHasMore = state.recentHasMore,
81-
onLoadMore = { section ->
82-
vm.loadMore(
83-
when (section) {
84-
GutenbergHomeSection.POPULAR -> GutenbergListTarget.POPULAR
85-
GutenbergHomeSection.RECENT -> GutenbergListTarget.RECENT
86-
},
87-
)
88-
},
89-
isPresentOnDevice = vm::isPresentOnDevice,
90-
onAdd = vm::sendToWatch,
56+
state.noWatch -> WatchReconnectPrompt(
57+
text = stringResource(R.string.no_watch),
58+
onReconnect = vm::reconnect,
9159
)
60+
else -> {
61+
SearchBar(
62+
value = state.query,
63+
onValueChange = vm::onQueryChange,
64+
onSubmit = vm::submitSearch,
65+
)
66+
GutenbergContent(state, homeSection, { homeSection = it }, vm)
67+
}
9268
}
9369
}
9470
}
@@ -104,6 +80,49 @@ fun GutenbergScreen(vm: GutenbergViewModel, onBack: () -> Unit) {
10480
}
10581
}
10682

83+
@Composable
84+
private fun GutenbergContent(
85+
state: GutenbergViewModel.UiState,
86+
homeSection: GutenbergHomeSection,
87+
onHomeSectionChange: (GutenbergHomeSection) -> Unit,
88+
vm: GutenbergViewModel,
89+
) {
90+
when {
91+
state.loading && state.visibleBooks.isEmpty() -> CenteredProgress()
92+
state.showingSearch && state.searchResults.isEmpty() -> CenteredText("No results.")
93+
state.showingSearch -> ResultsList(
94+
results = state.searchResults,
95+
downloadingId = state.downloadingId,
96+
loadingMore = state.loadingMore,
97+
hasMore = state.searchHasMore,
98+
onLoadMore = { vm.loadMore(GutenbergListTarget.SEARCH) },
99+
isPresentOnDevice = vm::isPresentOnDevice,
100+
onAdd = vm::sendToWatch,
101+
)
102+
state.popularBooks.isEmpty() && state.recentReleases.isEmpty() -> CenteredText("No books.")
103+
else -> HomeSections(
104+
popularBooks = state.popularBooks,
105+
recentReleases = state.recentReleases,
106+
selectedSection = homeSection,
107+
onSectionChange = onHomeSectionChange,
108+
downloadingId = state.downloadingId,
109+
loadingMore = state.loadingMore,
110+
popularHasMore = state.popularHasMore,
111+
recentHasMore = state.recentHasMore,
112+
onLoadMore = { section ->
113+
vm.loadMore(
114+
when (section) {
115+
GutenbergHomeSection.POPULAR -> GutenbergListTarget.POPULAR
116+
GutenbergHomeSection.RECENT -> GutenbergListTarget.RECENT
117+
},
118+
)
119+
},
120+
isPresentOnDevice = vm::isPresentOnDevice,
121+
onAdd = vm::sendToWatch,
122+
)
123+
}
124+
}
125+
107126
@Composable
108127
private fun GutenbergSnackbarHost(
109128
state: GutenbergViewModel.UiState,

companion/src/main/kotlin/com/fredapp/wbooksutil/GutenbergViewModel.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class GutenbergViewModel(application: Application) : AndroidViewModel(applicatio
3737
val errorMessage: String? = null,
3838
val lastSentTitle: String? = null,
3939
val lastStatusMessage: String? = null,
40+
val noWatch: Boolean = false,
4041
) {
4142
val showingSearch: Boolean
4243
get() = query.trim().isNotEmpty()
@@ -86,6 +87,12 @@ class GutenbergViewModel(application: Application) : AndroidViewModel(applicatio
8687
}
8788
}
8889

90+
fun reconnect() {
91+
_state.value = _state.value.copy(noWatch = false, errorMessage = null)
92+
loadHomeSections()
93+
refreshDeviceBooks()
94+
}
95+
8996
fun onQueryChange(value: String) {
9097
val previous = _state.value
9198
_state.value = previous.copy(
@@ -256,13 +263,7 @@ class GutenbergViewModel(application: Application) : AndroidViewModel(applicatio
256263
lastSentTitle = book.title,
257264
)
258265
result is WatchRepository.Result.NoWatch ->
259-
_state.value.copy(
260-
downloadingId = null,
261-
downloadingTitle = null,
262-
downloadProgressBytes = 0L,
263-
downloadProgressTotal = -1L,
264-
errorMessage = "No watch connected",
265-
)
266+
_state.value.disconnectedCopy()
266267
result is WatchRepository.Result.Error ->
267268
_state.value.copy(
268269
downloadingId = null,
@@ -333,12 +334,26 @@ class GutenbergViewModel(application: Application) : AndroidViewModel(applicatio
333334
deviceBookTitleKeys = result.value.books.mapTo(mutableSetOf()) {
334335
it.title.normalizedTitleKey()
335336
},
337+
noWatch = false,
336338
)
337339
}
338-
else -> Unit
340+
is WatchRepository.Result.NoWatch -> _state.value = _state.value.disconnectedCopy()
341+
is WatchRepository.Result.Error -> Unit
339342
}
340343
}
341344

345+
private fun UiState.disconnectedCopy(): UiState = copy(
346+
downloadingId = null,
347+
downloadingTitle = null,
348+
downloadProgressBytes = 0L,
349+
downloadProgressTotal = -1L,
350+
noWatch = true,
351+
loading = false,
352+
loadingMore = false,
353+
errorMessage = null,
354+
lastStatusMessage = null,
355+
)
356+
342357
private fun mergeBooks(current: List<GutenbergBook>, incoming: List<GutenbergBook>): List<GutenbergBook> {
343358
val seen = current.mapTo(mutableSetOf()) { it.id.ifEmpty { it.downloadUrl } }
344359
val merged = current + incoming.filter { seen.add(it.id.ifEmpty { it.downloadUrl }) }

companion/src/main/kotlin/com/fredapp/wbooksutil/SettingsScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ fun SettingsScreen(vm: SettingsViewModel, onBack: () -> Unit) {
8383
when (val s = state) {
8484
is SettingsViewModel.SyncState.Idle,
8585
SettingsViewModel.SyncState.Loading -> CenteredProgress()
86-
SettingsViewModel.SyncState.NoWatch -> CenteredText(
87-
"No watch connected. Connect your watch to view and edit settings.",
86+
SettingsViewModel.SyncState.NoWatch -> WatchReconnectPrompt(
87+
text = "No watch connected. Connect your watch to view and edit settings.",
88+
onReconnect = vm::refresh,
8889
)
8990
is SettingsViewModel.SyncState.Error -> CenteredText("Couldn't reach watch: ${s.message}")
9091
is SettingsViewModel.SyncState.Refreshing -> Column(Modifier.fillMaxSize()) {

0 commit comments

Comments
 (0)