diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 21:07:05 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 21:07:05 +0000 |
| commit | d7c05609692153670dc136082da4e0566d7838a6 (patch) | |
| tree | ae0a4b5ca3a04eb61cba73fa8a3c3c873faf984f /app/src/main/java/org/terst/feedreader/data | |
| parent | 8e06fc84c0b540f8ba87afa19d4c08eab7af80d9 (diff) | |
Remove reader-mode heuristics entirely; explicit per-feed opt-in only
Reader mode was still switching kottke articles even after the v3
opt-out and the "un-hide only if thin" fallback -- both were still
heuristic (content-length-based), and kottke's posts are routinely short
by design, not paywalled, so the heuristics kept firing on ordinary
content. Removed the auto-extract-on-thin-content logic entirely: reader
mode now NEVER turns on by itself, under any condition.
- FeedEntity.readerModeDisabled -> readerModeEnabled (opt-in, default off
for every feed -- schema v4, full table rebuild since Room's migration
validation requires an exact column match and a bare ADD COLUMN left
the old column orphaned; reproduced live as a crash, "Migration didn't
properly handle: feeds").
- The reader's toggle icon now sets reader mode for the article's WHOLE
FEED, persists it immediately, and confirms via snackbar ("Reader mode
enabled/disabled for X") -- both in the reader screen and the Feeds
list's own toggle. This is the only way reader mode ever turns on.
- ReaderExtractor: replaced the "un-hide everything if thin" paywall
fallback (which is what pulled kottke's site footer into the result --
confirmed live, twice) with one that only un-hides elements whose
class/id actually names them as paywall/premium/subscriber content
(West Hawaii Today's hidden div is literally class="paywall
paywall_<id>"). This turned out not to be the kottke bug's actual cause
either -- see below -- but it's a real, independent correctness
improvement over the previous blanket version, so it stays.
- Also added: noscript-wrapped lazy-load image fallback (a common
pattern pairs a placeholder <img> with a real one inside <noscript>,
which our JS-enabled extraction WebView never parses into DOM per the
HTML spec) and lazy data-src/srcset resolution generally.
Root cause of the kottke footer bug, found by fetching and inspecting
the actual page structure rather than guessing again: it's not a
CSS-hidden-element issue at all. kottke's real post content lives in a
plain, undecorated <div class="post"> that's a sibling (not ancestor) of
an ads container and a "Socials & More" footer widget, all under one
shared <div class="grid-container">. Because kottke's posts are so
short, Readability's generic paragraph-density scoring doesn't find
enough signal in .post alone and climbs to a broader container that
happens to include the neighboring footer. No un-hide logic is involved
at any point -- this reproduces identically on the plain, unmodified
Readability pass. This is a genuine limit of heuristic content
extraction on very short-form posts, not something to keep patching with
more heuristics. Reset kottke's setting back to off (its correct
default) after testing.
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/data')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/data/AppDatabase.kt | 45 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/data/Entities.kt | 4 |
2 files changed, 45 insertions, 4 deletions
diff --git a/app/src/main/java/org/terst/feedreader/data/AppDatabase.kt b/app/src/main/java/org/terst/feedreader/data/AppDatabase.kt index 4cf0ecd..441556b 100644 --- a/app/src/main/java/org/terst/feedreader/data/AppDatabase.kt +++ b/app/src/main/java/org/terst/feedreader/data/AppDatabase.kt @@ -9,7 +9,7 @@ import androidx.room.migration.Migration @Database( entities = [FeedEntity::class, ArticleEntity::class, FilterRuleEntity::class], - version = 3, + version = 4, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { @@ -31,19 +31,58 @@ abstract class AppDatabase : RoomDatabase() { } } - // v3: added FeedEntity.readerModeDisabled (per-feed reader-mode opt-out). + // v3: added FeedEntity.readerModeDisabled (per-feed reader-mode opt-out, paired with + // an auto-extract-on-thin-content heuristic). Superseded by v4 below. private val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE feeds ADD COLUMN readerModeDisabled INTEGER NOT NULL DEFAULT 0") } } + // v4: replaced readerModeDisabled with readerModeEnabled -- reader mode is no longer + // ever auto-triggered by a content-length heuristic (that's what kept "auto-switching" + // kottke articles even after the v3 opt-out); it's now a plain per-feed opt-in, set + // only by explicitly tapping the reader-mode toggle. Starts everyone fresh at off + // rather than trying to translate the old opt-out semantics, which don't map cleanly + // onto "opt in". A plain ADD COLUMN + leaving the old one in place (as v1->v2 and + // v2->v3 do) crashes here: Room's post-migration validation requires the table's + // columns to match FeedEntity's fields EXACTLY, and readerModeDisabled isn't a field + // anymore -- confirmed live ("Migration didn't properly handle: feeds"). SQLite has no + // DROP COLUMN on the versions bundled with older Android, so this rebuilds the table + // instead: create with the new shape, copy the columns that still exist, swap in. + private val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE feeds_new ( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + siteUrl TEXT, + lastFetchedAt INTEGER, + etag TEXT, + lastModified TEXT, + lastError TEXT, + readerModeEnabled INTEGER NOT NULL DEFAULT 0 + ) + """.trimIndent() + ) + db.execSQL( + """ + INSERT INTO feeds_new (id, title, siteUrl, lastFetchedAt, etag, lastModified, lastError) + SELECT id, title, siteUrl, lastFetchedAt, etag, lastModified, lastError FROM feeds + """.trimIndent() + ) + db.execSQL("DROP TABLE feeds") + db.execSQL("ALTER TABLE feeds_new RENAME TO feeds") + } + } + fun get(context: Context): AppDatabase = instance ?: synchronized(this) { instance ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "feedreader.db" - ).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build().also { instance = it } + ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build().also { instance = it } } } } diff --git a/app/src/main/java/org/terst/feedreader/data/Entities.kt b/app/src/main/java/org/terst/feedreader/data/Entities.kt index f943f78..f4be19e 100644 --- a/app/src/main/java/org/terst/feedreader/data/Entities.kt +++ b/app/src/main/java/org/terst/feedreader/data/Entities.kt @@ -13,7 +13,9 @@ data class FeedEntity( val etag: String? = null, // conditional-GET caching (If-None-Match) to avoid re-downloading unchanged feeds val lastModified: String? = null, // conditional-GET caching (If-Modified-Since) val lastError: String? = null, // set when a fetch fails; cleared on the next successful fetch - val readerModeDisabled: Boolean = false // per-feed opt-out: never auto/manually switch to Readability for this feed's articles (e.g. a feed whose own formatting is better than what extraction produces) + val readerModeEnabled: Boolean = false // per-feed opt-IN, set only by explicitly tapping the reader-mode + // toggle (never automatic/heuristic) -- once on, articles from this feed default to the + // extracted view and auto-extract on open instead of requiring a per-article tap each time. ) @Entity(tableName = "articles") |
