From 2b0b24da30047e906159a0696f6b2a03efbbeddf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 08:03:09 +0000 Subject: [PATCH 1/4] Add ArticleWidgetContentProvider for list widget data access Exposes content://me.ash.reader.widget.articles/{widgetId} so external processes (e.g. SmartSpacer) can query unread articles for a given widget ID. Returns id, feed_name, title columns capped at 15 rows, backed by the existing WidgetRepository/DataSource config storage. https://claude.ai/code/session_015nuC2FAJoQJx3qSwMGc7E3 --- app/src/main/AndroidManifest.xml | 4 ++ .../ui/widget/ArticleWidgetContentProvider.kt | 51 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 app/src/main/java/me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11fb45d60..c0340448b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,6 +110,10 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + ?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?, + ): Cursor? { + if (uriMatcher.match(uri) != MATCH_ARTICLES) return null + val widgetId = uri.lastPathSegment?.toIntOrNull() ?: return null + + val repo = WidgetRepository.get(context!!) + val config = runBlocking { repo.getConfig(widgetId) } + val articles = runBlocking { repo.getData(config.dataSource).first() }.articles + + val cursor = MatrixCursor(COLUMNS) + articles.take(15).forEach { article -> + cursor.addRow(arrayOf(article.id, article.feedName, article.title)) + } + return cursor + } + + override fun getType(uri: Uri): String? = null + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 +} From 775c34e62c970146519137df5491edba685edecf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 08:23:41 +0000 Subject: [PATCH 2/4] Fix TTS batching to send whole paragraphs instead of single lines Html.fromHtml() converts
to \n and

/headings to \n\n, so the previous split("\n") was breaking mid-paragraph at every
. Now splits on two or more consecutive newlines (paragraph boundaries) and collapses any remaining inline \n into spaces, so each TTS utterance is a full paragraph. https://claude.ai/code/session_015nuC2FAJoQJx3qSwMGc7E3 --- .../java/me/ash/reader/infrastructure/android/TextToSpeech.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt b/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt index faefa5152..a79231bc1 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt @@ -84,7 +84,9 @@ class TextToSpeechManager @Inject constructor( .firstOrNull()?.locale } - val textSegments = text.split("\n").filterNot { it.isBlank() } + val textSegments = text.split(Regex("\n{2,}")) + .map { it.replace('\n', ' ').trim() } + .filterNot { it.isBlank() } val total = textSegments.size state = State.Reading(0, total) From 374fcceca659028235f212be4717fe4e55c9149d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 05:34:25 +0000 Subject: [PATCH 3/4] Fix TTS to send whole paragraphs regardless of HTML structure The previous fix (split on \n{2,}) still split mid-paragraph when RSS content wrapped each sentence in its own

tag, because Html.fromHtml emits \n\n at every

boundary. Now collapses all whitespace (including paragraph newlines) into a single space and only splits when the text exceeds the TTS engine's max input length (4000 chars), splitting at the last word boundary before the limit. For typical RSS descriptions this means one utterance per article read. https://claude.ai/code/session_015KrBpHUWpJWmDncbCkxd3f --- .../reader/infrastructure/android/TextToSpeech.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt b/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt index a79231bc1..7dd525cc8 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/android/TextToSpeech.kt @@ -84,9 +84,17 @@ class TextToSpeechManager @Inject constructor( .firstOrNull()?.locale } - val textSegments = text.split(Regex("\n{2,}")) - .map { it.replace('\n', ' ').trim() } - .filterNot { it.isBlank() } + val normalized = text.replace(Regex("\\s+"), " ").trim() + val maxLen = TextToSpeech.getMaxSpeechInputLength() + val textSegments = buildList { + var remaining = normalized + while (remaining.length > maxLen) { + val split = remaining.lastIndexOf(' ', maxLen).takeIf { it > 0 } ?: maxLen + add(remaining.substring(0, split)) + remaining = remaining.substring(split).trimStart() + } + if (remaining.isNotBlank()) add(remaining) + } val total = textSegments.size state = State.Reading(0, total) From c9fbc1d101555a1183d3f999cbabd2d17697f776 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 11:03:09 +0000 Subject: [PATCH 4/4] Expose publish date in ArticleWidgetContentProvider Adds a 'date' column (Long, epoch milliseconds) to the content provider. Maps article.date.time from the domain Article into the widget Article data class and passes it through to the MatrixCursor addRow call. https://claude.ai/code/session_015KrBpHUWpJWmDncbCkxd3f --- .../me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt | 4 ++-- app/src/main/java/me/ash/reader/ui/widget/WidgetRepository.kt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt b/app/src/main/java/me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt index c479099d3..fb27ab3a0 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/ArticleWidgetContentProvider.kt @@ -13,7 +13,7 @@ class ArticleWidgetContentProvider : ContentProvider() { companion object { const val AUTHORITY = "me.ash.reader.widget.articles" - val COLUMNS = arrayOf("id", "feed_name", "title") + val COLUMNS = arrayOf("id", "feed_name", "title", "date") private const val MATCH_ARTICLES = 1 private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { @@ -39,7 +39,7 @@ class ArticleWidgetContentProvider : ContentProvider() { val cursor = MatrixCursor(COLUMNS) articles.take(15).forEach { article -> - cursor.addRow(arrayOf(article.id, article.feedName, article.title)) + cursor.addRow(arrayOf(article.id, article.feedName, article.title, article.date)) } return cursor } diff --git a/app/src/main/java/me/ash/reader/ui/widget/WidgetRepository.kt b/app/src/main/java/me/ash/reader/ui/widget/WidgetRepository.kt index fbc622b58..9e79207dd 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/WidgetRepository.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/WidgetRepository.kt @@ -116,6 +116,7 @@ constructor( imgUrl = article.img, feedName = feed.name, id = article.id, + date = article.date.time, ) } } @@ -179,4 +180,5 @@ data class Article( val title: String, val imgUrl: String? = null, val feedName: String, + val date: Long = 0L, )