Commit Graph
100 Commits
Author SHA1 Message Date
AntsyLich 9612d955db Release v0.21.2 2026-10-13 01:52:15 +06:00
AntsyLich 5ca2a39328 Fix concurrent Cloudflare challenges each opening a WebView
Closes #3858

Co-authored-by: tigeryu8900 <tigeryu8900@gmail.com>
(cherry picked from commit 5a5ff22c37)
2026-10-13 01:52:15 +06:00
AntsyLich fb2b224030 Fix some downloads not being recognized
(cherry picked from commit f0f1e4de5a)
2026-10-13 01:39:01 +06:00
AntsyLich ee454f3a82 Ignore long strip taps confirmed after the reader is gone
(cherry picked from commit c371d6331d)
2026-10-13 01:39:01 +06:00
AntsyLich 4fd0081817 Fail the install when nothing can confirm it instead of crashing
(cherry picked from commit 2df48e19a1)
2026-10-13 01:39:01 +06:00
AntsyLich 68dbd7a185 Show invalid filters from extensions instead of crashing
(cherry picked from commit c9e1b6e96e)
2026-10-13 01:39:01 +06:00
AntsyLich 7475f4554c Insert split pages only into the chapter they belong to
(cherry picked from commit 3156b4a1c4)
2026-10-13 01:39:00 +06:00
AntsyLich cca6b5e2e7 Solve Cloudflare challenges of non-GET requests on the site's root
Closes #3909

(cherry picked from commit 7ae858e602)
2026-10-13 01:39:00 +06:00
AntsyLich 4c316ccc3a Update navigationevent to stop back gestures crashing closed screens
(cherry picked from commit 43de88c718)
2026-10-13 01:39:00 +06:00
AntsyLich 7c7392a533 Guard the database update against duplicates and damaged indexes
(cherry picked from commit ea2b73ad78)
2026-10-13 01:39:00 +06:00
AntsyLich e9670d8a12 Release v0.21.1 2026-10-12 14:09:01 +06:00
AntsyLich 87e808548b Merge branch 'main' into 0.21.0
# Conflicts:
#	CHANGELOG.md
2026-10-12 14:04:00 +06:00
AntsyLich 51b61a36e8 Keep the pager on its page when chapters are added before layout
Closes #4079
2026-10-12 13:56:33 +06:00
AntsyLich 010ac71a84 Revert "Always update uninitialized library entries (#3179)"
This reverts commit 8c0419657b.

Closes #4081
2026-10-12 13:56:33 +06:00
AntsyLich 9dbb7148c5 Show failed migrations instead of waiting on them forever 2026-10-12 13:56:33 +06:00
AntsyLich a999e49f2f Index duplicate entries so moving their chapters doesn't take hours 2026-10-12 13:56:33 +06:00
AntsyLich ae749bb4f8 Only cancel the install of the extension being cancelled 2026-10-12 13:56:33 +06:00
AntsyLich 649fc83ffa Keep the app alive when a WebView renderer dies 2026-10-12 13:56:32 +06:00
AntsyLich 6dffca1593 Keep a missing date added as is when merging it into favorite_at 2026-10-12 13:56:32 +06:00
AntsyLich 07b9fb695c Fix extension installer crash when cancelling off the main thread 2026-10-12 13:56:32 +06:00
AntsyLich c279291e29 Fix manga screen FAB going under the taskbar on tablets
Closes #4077
2026-10-12 13:51:25 +06:00
AntsyLich abb126ad20 Fix startup crash on some Samsung Android 9 to 11 devices
Closes #4078
2026-10-12 13:51:25 +06:00
AntsyLich 8457fb91e0 Fix chapter refresh failing in locales with non-ASCII digits
Closes #4075
2026-10-12 13:51:25 +06:00
AntsyLich 143a060ac9 Release v0.21.0
Keeps the changelog entries that f45d0ad31d removed for the release under
Unreleased, since main still has the WebGPU reader and the new image
decoder.

(cherry picked from commit 79fc3a0a8a)
2026-10-12 02:40:27 +06:00
AntsyLich 8912ef5b5c Regenerate baseline profiles
(cherry picked from commit c8b820a08f)
2026-10-12 02:34:27 +06:00
AntsyLich 79fc3a0a8a Release v0.21.0 2026-10-12 01:37:06 +06:00
AntsyLich c8b820a08f Regenerate baseline profiles 2026-10-12 01:37:05 +06:00
AntsyLich f45d0ad31d Remove the WebGPU reader and go back to the old image decoder
Brings back SSIV and the image decoder from v0.20.4 with their settings,
except "Use legacy decoder for long strip reader", which long strip
pages drawn as strips replace.
2026-10-12 01:14:22 +06:00
AntsyLichandMatheus William b5856135c5 Fix webtoon scroll stutter between pages
Closes #4026

Co-authored-by: Matheus William <matheuswr89@outlook.com>
2026-10-12 00:37:45 +06:00
AntsyLich bce1f71958 Update changelog 2026-10-11 21:50:32 +06:00
AntsyLich d58130a5fc Check for app updates every 6 hours while the app is open 2026-10-11 21:45:51 +06:00
AntsyLich e562a9b4f8 Show a wavy progress ring while a chapter downloads 2026-10-11 21:33:35 +06:00
AntsyLich ba44815254 Count early, late and backdated releases when predicting next update 2026-10-11 21:33:35 +06:00
AntsyLich fc20410ceb Allow skipping an app update version
Closes #3969
Closes #3700
2026-10-11 15:38:14 +06:00
AntsyLich b454c1b469 Keep the migration screen up for at least a second 2026-10-11 15:11:07 +06:00
AntsyLich f02bf5ba7e Queue many chapters without probing each one on disk 2026-10-11 14:54:32 +06:00
AntsyLich ba16158b13 Show download progress and open the manga screen with less work 2026-10-11 13:43:35 +06:00
AntsyLich 8066b8dfc9 Read extension stores once at startup
Refreshing stores now fetches them all first and writes them in one transaction.
2026-10-11 13:43:34 +06:00
AntsyLich 260e31cd2c Improve performance across screens (#4071)
* Keep screens loaded while they're hidden and rebuild the library faster

* Count chapters in SQL for the migration list

* Show the extension list sooner and check trust against loaded stores

* Cancel the previous tracker search when searching again

* Write reader progress less often and keep its work off the main thread

* Stop rebuilding the settings search index on every key

* Watch updates for widgets only while one is placed

The query re-ran on every chapter or manga write even with no widget on
the home screen.

* Observe download pages and limit how often their progress updates

* Throttle the library update notification and write fetch data together
2026-10-11 04:28:52 +00:00
AntsyLich b15af71b94 Load installed extension and source icons through Coil 2026-10-11 05:04:59 +06:00
AntsyLich 3e5d593e28 Implement parallel page loading and preloading in reader (#2986) 2026-10-10 19:14:53 +00:00
AntsyLich c46317f099 Move blocking work off the main thread and fix threading races (#4068)
* Defer startup work until first use

* Move cover work off the main thread

* Make tracker calls main-safe and token refresh thread-safe

Kitsu, Shikimori and MangaBaka refresh their token under a lock like
MAL, so parallel calls no longer refresh it twice.

* Fix extension threading and make installs cancellable

The store service read response bodies on Main, which failed with
NetworkOnMainThreadException for http stores.

* Move reader work off the main thread and save progress in order

* Serialize the downloader and fix download threading

* Stop blocking on WorkManager status queries

Also removes NotificationReceiver's open chapter action: nothing sends
it, and its handler used runBlocking.

* Start migration jobs on the migrator's dispatcher

They started undispatched, so each ran on the caller's thread until its
first suspension.

* Run screen and view model work on fitting dispatchers

* Launch work that outlives a screen on the app scope

A launch on another dispatcher is dropped if the screen closes before it
starts, which dialogs that dismiss right away often do. Only the UI
follow-up stays on viewModelScope.

* Remove the coroutine helper extensions

They hid which dispatcher and scope a call used, and the GlobalScope
ones hid that it was global.
2026-10-10 17:08:49 +00:00
AntsyLich f9c5fa9011 Log app scope failures instead of crashing outside debug builds 2026-10-10 22:51:17 +06:00
AntsyLich a82d27c3a6 Keep the chapter selection when new chapters download after a refresh 2026-10-10 22:37:58 +06:00
AntsyLich 611578313e Index a finished download found on disk instead of downloading it again 2026-10-10 20:19:06 +06:00
AntsyLich f921ec07a6 Use an app scoped CoroutineScope
Replaces the eight ad hoc application lifetime scopes and the GlobalScope
launchUI/launchIO/launchNow helpers with a single injected
CoroutineScope(SupervisorJob() + Dispatchers.IO), qualified by
@AppCoroutineScope.

Every scope this replaces was already on IO, or named IO explicitly, so
call sites keep their dispatcher. The two that need the main thread use
scope.launchUI. Scopes with a real cancellation point keep their own:
HttpPageLoader, ShizukuInstaller and the reader viewers. Only
HttpPageLoader's page list cache save moves to the app scope, since it
runs after recycle() has cancelled the loader's own scope.

NotificationReceiver now holds the broadcast open with goAsync() while
its work runs, so the process isn't torn down mid update.

This reverts the revert of #3403. It was reverted for #3429, where
restore() dropped every download because SourceManager.get() returned
null until the extensions had loaded. That surface suspends since #3869,
so the race is gone.
2026-10-10 17:37:45 +06:00
AntsyLich e21414b229 Drop the legacy chapter model and narrow last page read to an Int
eu.kanade.tachiyomi.data.database.models.Chapter predates SQLDelight. It
survived because it implements SChapter, so ReaderChapter could hand the
same object to the database and to the source.

ReaderChapter now holds the domain Chapter and converts at the three
points that actually need a source model: getPageList, getFormat and
getChapterUrl. The domain model is immutable, so the reader replaces the
chapter rather than writing through it, behind an update function so the
field cannot be reassigned from outside. Equality stays on the chapter it
was loaded with, which keeps the identity comparisons in the viewers
stable while progress is recorded against it.

updateChapterProgress no longer mutates the chapter and reads the values
straight back out to build the update it persists; it computes them once
and passes them. Marking a chapter read moves out of the function named
for completing it and into the same place the page is recorded.

Removing the interface also drops the nullable id it carried, along with
three non-null assertions and a Float to Double conversion that the
compiler had been warning about.

A page position is an index into a chapter's pages, so it cannot exceed
Int range, but SQLite has one integer type and the column came back as a
Long. That width reached the domain model and then every caller either
carried it or converted, and the viewers had to narrow it again to index
a list with.

Declaring the column INTEGER AS Int moves the narrowing into a column
adapter, so it happens once where the value stops being a database
number. No migration: SQLite still stores an INTEGER, only the type
SQLDelight generates changes.

Backups are the exception and keep converting, because the field is on
the wire as int64 and has to stay that way. That is also the only place
the value arrives from outside the app, where a malformed file could
carry something that does not fit and truncate to a negative index, so
it is clamped rather than cast.
2026-10-10 17:37:44 +06:00
AntsyLich e42e01049d Recognize volumes and use neighbouring chapters for chapter numbers
Sync now recognizes the chapters of a manga together, which tells what a
name alone can't:
- Numbers every similar name shares, like the group in "Group 2 Chapter
  386", aren't the chapter number.
- Chapters without a number get one from the chapters around them, as long
  as none of them is in another volume, since numbers may start over in the
  next one. Between two chapters they get the missing whole numbers when
  those fit (2, ?, 4 -> 3), or .5, .6 and so on (10, ?, ?, 11 -> 10.5,
  10.6). Bonus chapters after the newest chapter go above it. A prologue
  gets 0, and volume 0 when the chapter after it has a volume.

Recognition returns a ChapterNumbering of the volume and chapter number as
strings, null when there is none. Sub-chapters keep their letter (19a) and
parts become one (Chapter 1.5 Part I -> 1.5a); sync turns the chapter
number into a double the way letters always counted. Volumes, seasons
included, are recognized from names and are no longer taken as the chapter
number. Sync only uses the chapter number for now.
2026-10-10 17:37:44 +06:00
AntsyLich 6c1b0702ba Use the beta Compose BOM
The alpha BOM's 2026.10.00 has the same Compose and Material 3 versions as the
beta one, but it keeps material3-adaptive on alpha. Tracker search now reads the
locale from composition, since Locale.current is deprecated.

Closes #4065
2026-10-10 17:37:44 +06:00
AntsyLich 292381662a Follow weekday and monthly release patterns for next update and back it up 2026-10-10 17:37:43 +06:00
AntsyLich 44c1e1915d Store every date in the database as an Instant 2026-10-10 17:37:43 +06:00
AntsyLich 8972882a34 Use plain icon buttons for extension actions 2026-10-10 17:37:43 +06:00
AntsyLichandJammo e9234f205d Match setting group headings in settings search (#4044)
Closes #4044
Fixes #3098

Co-authored-by: Jammo <8018115+kaosjammo@users.noreply.github.com>
2026-10-10 17:36:21 +06:00
AntsyLichandAddictToTheKey be44a81fb6 Keep listing local chapters when one archive can't be read (#3997)
Closes #3997
Fixes #2548

Co-authored-by: AddictToTheKey <addicttothekey@local>
2026-10-10 17:36:20 +06:00
AntsyLichandChopper 88d4071cce Don't let the stored download index overwrite a startup rescan (#3968)
Closes #3968
Fixes #3526
Fixes #2808
Fixes #1429

Co-authored-by: Chopper <156493704+choppeh@users.noreply.github.com>
2026-10-10 17:36:20 +06:00
AntsyLichandJoseph f28b2d887e Cancel queued downloads of chapters the source removed (#3845)
Closes #3845
Fixes #3811
Fixes #3501

Co-authored-by: Joseph <50675784+JoJoKorok@users.noreply.github.com>
2026-10-10 17:36:20 +06:00
AntsyLichandAMendoza 287cbbb986 Keep the last update time on an empty Updates screen (#3790)
Closes #3790
Closes #4052
Fixes #3647

Co-authored-by: AMendoza <allanmaleman@gmail.com>
2026-10-10 17:36:20 +06:00
AntsyLichandNarwhalHorns 40f552981a Don't match every library entry as a duplicate of a blank title (#3308)
Closes #3308

Co-authored-by: NarwhalHorns <onefailedgamer@gamil.com>
2026-10-10 17:36:19 +06:00
AntsyLichandNarwhalHorns 8c0419657b Always update library entries that were never initialized (#3179)
Closes #3179
Closes #3144

Co-authored-by: NarwhalHorns <onefailedgamer@gamil.com>
2026-10-10 17:36:19 +06:00
AntsyLichandLeodyver Semilla 36ec043a5c Delete downloads of duplicates read along with a chapter (#3030)
Closes #3030
Fixes #2454

Co-authored-by: Leodyver Semilla <leodyversemilla07@gmail.com>
2026-10-10 17:36:18 +06:00
AntsyLichandQuickdev 0c1be369b0 Ask to add an entry to the library before editing its cover (#2265)
Closes #2265

Co-authored-by: Quickdev <fyfrive@gmail.com>
2026-10-10 17:36:18 +06:00
AntsyLich a80027ff36 Update icons and move remaining to Material Symbols (#4057)
* Update Simple Icons to 16.34.0

The brand paths are unchanged; only the version in the headers moves.

* Use the current Material Symbols names

Several symbols were renamed after the Valkyrie migration. people_alt,
new_releases, screen_lock_*, screen_rotation and stay_current_* are only
aliases in the font now, for group, verified and the mobile_* symbols.
done, push_pin and expand_less/expand_more become check, keep and
keyboard_arrow_up/down, which draw the same or an almost identical glyph.

* Use filled icons where the Material Symbols migration dropped the fill

The migration mapped Filled and Default references to the outline symbols,
but the classic glyph was often solid, and Outlined Warning, Error and
Pause were solid as well. Each call site was compared against the rendered
classic glyph, and the solid ones now use RoundedFilled. view_list needs an
auto-mirrored filled pack. The outline SVGs this leaves unused are removed.

* Show a compact grid icon for the compact display modes

Compact and cover-only grids showed the same view_module icon as the
comfortable grid.

* Generate notification and layout drawables from Material Symbols

Notifications and XML layouts can't use ImageVectors, so they relied on
hand-kept copies of classic Material drawables. A build-logic task now
converts an opt-in list of the module's SVGs with AGP's Svg2Vector into
generated resources. sdk-common is taken from AGP's own dependency metadata,
so its version always matches AGP.

Compose call sites that only needed a drawable (tab close, crop) use the
ImageVectors instead. The icons module's R class must be imported as
MaterialSymbolsR, enforced by spotless, so it isn't mistaken for the app's R.

* Redraw custom icons in the Material Symbols style

The reading mode, crop off, mark previous as read and chapter download
icons were drawn in the classic style. They are rebuilt from Material
Symbols shapes (mobile_gear, mobile_arrow_right/down, crop,
arrow_circle_down, downloading) in a new custom-material-symbols module;
each SVG header lists where its parts come from. The default reading mode
and incognito use mobile_gear and eyeglasses_2 as they are.

ReadingMode holds an ImageVector now, and the module's R class follows the
same alias rule as MaterialSymbolsR.

* Use Material Symbols for the launcher shortcuts

Updates uses release_alert because the symbol named new_releases is now a
check badge.

* Rename the animated icon drawables

anim_*_enter no longer fits now that the icons play in both directions.
This is a pure rename so the following commits show only content changes.

* Use Material Symbols for the bottom navigation icons

The library, updates, history and browse animations are rebuilt from the
outline and filled symbols but keep their original motion: the white inside
is pushed into the ribbon, the "!" and the needle, and the history ring
still spins. Updates uses release_alert, like the shortcut. More already
matches more_horiz and is unchanged.

* Use rounded bars for the manga description caret

The animation is unchanged; the bars get rounded ends to match the
Material Symbols stroke.

* Settle the browse tab icon when selecting and deselecting

The compass turns a full turn and settles with overshoot both ways, instead
of deselecting by playing the select animation backwards. A
rememberAnimatedVectorPainter overload takes separate enter and exit
animations and doesn't animate on the first composition.
2026-10-09 17:50:12 +00:00
AntsyLich dd46413e10 List MangaBaka first among trackers 2026-10-09 19:16:42 +06:00
AntsyLich fd4f73c908 Show a screen while the app migrates after an update 2026-10-07 22:22:32 +06:00
AntsyLich a6ee4bc596 Fix library range selection crash across categories 2026-10-07 19:59:52 +06:00
AntsyLich 6d4ed6cb22 Use the backport class loader for extensions before Android 17
ART maps app images compiled for the built-in DelegateLastClassLoader's class loader chain, and before Android 17 a damaged one crashes every launch until the app is updated.
2026-10-07 19:59:52 +06:00
AntsyLich 76b146f3d4 Show the updates widget's empty and locked text 2026-10-07 19:59:52 +06:00
AntsyLich 5910a51eb4 Keep the names of uninstalled sources across backup restores 2026-10-07 19:59:52 +06:00
AntsyLich 1e471a414c Stop updates showing a finished chapter as still downloading 2026-10-07 19:59:52 +06:00
AntsyLich 06d6128116 Let R8 shrink and optimize app code
Names stay as they are, since -dontobfuscate is unchanged. Injekt reads the injected type from the generic superclass of its type tokens, so those keep their Signature attribute.
2026-10-07 19:59:52 +06:00
AntsyLich 6a329386c2 Build a chapter's folder name variants from parts computed once 2026-10-07 19:59:52 +06:00
AntsyLich 179bfaca72 Load all tracks in one query for statistics 2026-10-07 19:59:52 +06:00
AntsyLich 5df78e305e Read the library's last read time without joining history to chapters
Migration 18 replaces libraryView, so the version code is bumped.
2026-10-07 19:59:51 +06:00
AntsyLich 940272f75a Load tracks in the library only for tracker filters and sorting 2026-10-07 19:59:51 +06:00
AntsyLich 7ff97761b9 Make MangaViewModel collect the entry only while subscribed 2026-10-07 19:59:51 +06:00
AntsyLich 9a6dea804d Stop polling queued downloads for their page list 2026-10-07 19:59:51 +06:00
AntsyLich fb8e640d69 Look up queued and downloaded chapters once per chapter list 2026-10-07 19:59:51 +06:00
AntsyLich 1f85a5a8db Share one cover load between rows that show it at the same time 2026-10-07 19:59:51 +06:00
AntsyLich 730e43fd14 Download a source's next chapter while the current one finishes
Downloader now runs one job per source, whose chapters share the source's page slots, so the next chapter starts as soon as the current one's last pages have a slot.
2026-10-07 19:59:51 +06:00
AntsyLich 61cd786e7f Use WAL on every device 2026-10-07 19:59:51 +06:00
AntsyLich 6490c41e63 Wait for a canceled downloader job to finish before starting another
Page writes block, so a canceled job could still be writing pages when the next one started on the same pages, leaving duplicate "(1)" files and failed chapters.
2026-10-07 01:15:18 +06:00
AntsyLich a5d584eeb0 Sanitize the scanlator in chapter download folder names
A "/" in the scanlator nested the chapter in a folder of its own, where the download was never found again. Names built from the unsanitized scanlator are still looked up.
2026-10-07 01:05:30 +06:00
AntsyLich f027de8c2e Look up library item sources and download counts once per library update 2026-10-07 01:05:30 +06:00
AntsyLich 7aacaa3490 Keep the paused download notification after the worker stops
The paused notification reused the download worker's foreground notification
id, so it was removed together with the foreground service when the worker
finished.

Closes #2791
2026-10-04 13:36:43 +06:00
AntsyLich f82a7a68f4 Build download error notifications fresh each time
The warning and error notifications shared one builder for the life of
the app, so an error kept the first notification's timestamp and the
previous warning's expanded text, auto cancel and timeout.

Closes #3341
2026-10-04 13:11:06 +06:00
AntsyLich d1c5c12bdd Fix webtoon reader leaving a gap after zooming in a resized window
The reader measured its height once, so zooming after a split screen or
window resize pinned it to the old size until the reader was reopened.

Closes #1721
2026-10-04 13:11:06 +06:00
AntsyLich db45dda525 Make CoroutineWorker supertypes have Worker suffix 2026-10-02 10:23:07 +06:00
AntsyLich 9abc830769 Rename Preference.enabled to visible to correctly reflect usage 2026-10-02 10:23:07 +06:00
AntsyLich 9409740d7e Let SliderPreference collect its own preference
Closes #3958

Assisted-by: Claude:claude-opus-5-5
2026-10-02 10:23:07 +06:00
AntsyLich 38e93086c8 Configure database connections like Room 2026-10-02 10:23:07 +06:00
AntsyLich 01d302febd Let DownloadJob own the downloader lifecycle
Assisted-by: Claude:claude-opus-5-5
2026-10-02 10:23:07 +06:00
AntsyLich 6dfa10ffac Ensure tachiyomix public API is not removed by R8
Fixes #4027
2026-10-02 10:23:07 +06:00
AntsyLich 972c9d43da Bump versionCode to 33 2026-10-01 01:17:41 +06:00
AntsyLich 2d1d2e4cab Rework database queries for the new schema (#4022)
* query - category

Key every single-row statement on :id and list columns on one line. A
lookup by primary key no longer needs LIMIT 1, and resetting every
category's flags takes a value rather than a nullable nobody passed.

Assisted-by: Claude:claude-opus-5

* query - chapter

List columns in the table's order and name bind and mapper parameters
after the columns they fill. Inserts pass their arguments by name,
since reordering the columns put the upload and fetch dates side by
side with the same type.

With (manga_id, remote_url) unique, looking a chapter up by them no
longer needs LIMIT 1. Available scanlators come back distinct instead of
one row per chapter, which is all the only caller wanted. insert was
insertReturningId without the RETURNING, so restore uses the latter and
the former goes, along with the unused getChapterByUrl.

Inserting skips a chapter that a sync running alongside stored first,
rather than failing the whole batch and losing every new chapter of
that update.

Assisted-by: Claude:claude-opus-5

* query - excluded_scanlator

An entry excludes a scanlator at most once now. Inserting one that is
already excluded does nothing, so restoring a backup that lists one twice
drops the repeat instead of failing the entry, and restore no longer
reads the stored exclusions first to skip them itself.

Saving an unchanged selection no longer runs an empty delete, which
still made every chapter query run again.

Assisted-by: Claude:claude-opus-5

* query - extension_store

Take the upsert's new values from excluded and drop a WHERE its
conflict target already implies. That also retires a
`signing_key =: signingKey` that only parsed by accident.

Assisted-by: Claude:claude-opus-5

* query - history

Restoring an entry's history looked up the history row and, failing
that, the chapter for every item in the backup, one query each. It now
reads the entry's chapters and history once and matches them in memory,
which leaves getHistoryByChapterUrlAndMangaId without callers.

Creating a backup looked up the chapter of every history row one at a
time and failed on a row whose chapter was gone. It reads the entry's
chapters once and leaves such a row out.

The upsert takes its new values from excluded, drops a trailing WHERE
its conflict target already pins, and names its parameters after the
columns they fill.

Assisted-by: Claude:claude-opus-5

* query - manga

List columns in the table's order and name bind and mapper parameters
after the columns they fill, forwarding mapper arguments by name.

With (source_id, remote_url) unique, looking an entry up by them no
longer needs LIMIT 1, and fetching a network entry becomes a single
upsert that refreshes an existing row only while it is outside the
library, instead of an insert guarded by NOT EXISTS followed by an
update of the same row.

"Is anything read" checks use EXISTS rather than IN over a DISTINCT
subquery, category membership drops a coalesce on a column that is
never null, and the unused getAllManga goes. Applying the default
chapter flags to the library is one transaction instead of one per
entry, and restore checks backup entries against the stored urls as a
set rather than a list.

Assisted-by: Claude:claude-opus-5

* query - manga_category

An entry belongs to a category at most once now. Inserting a pairing
that already exists does nothing, so setting an entry's categories from
a list that names one twice no longer fails, which restore could produce
when two backup categories resolve to the same local one.

Assisted-by: Claude:claude-opus-5

* query - manga_track

List columns in the table's order and name bind and mapper parameters
after the columns they fill. That retires the sync-era :syncId,
:mediaId, :totalChapter and :trackingUrl.

Assisted-by: Claude:claude-opus-5

* query - source

List columns in the table's order and name the upsert's parameters
after its columns. The upsert takes the conflicting row's values from
excluded instead of rebinding them, and drops a WHERE its conflict
target already implies. The call passes its two string arguments by
name, since swapping them would still compile.

Assisted-by: Claude:claude-opus-5

* query - historyView

The latest read entry is read from the tables through the index on history's read time instead
of aggregating the whole view for one row: ~50ms to under 1ms on a 114k chapter library.

Assisted-by: Claude:claude-opus-5

* query - libraryView

List the columns like the other queries.

Assisted-by: Claude:claude-opus-5

* query - updatesView

Drop a coalesce on category_id, which is never null, from the queries
over updatesView, and the unused getRecentUpdates. The view definitions
themselves are unchanged, so no migration is needed.

The read and unread lookups become separate queries with the status
written into each. SQLite only uses a partial index when the query's own
condition matches it, which a bound parameter never does, so the unread
lookup the widget follows walked nearly the whole fetch date index,
~60ms each time on a 114k chapter library and after every chapter or
history write. With the status written in, it's served by the partial
index over unread chapters, ~8ms.

Assisted-by: Claude:claude-opus-5

* Query browse entries only while they are shown

Each loaded entry kept a live query for as long as the screen existed,
so every write to manga re-ran all of them. An entry now starts from the
row the listing stored and queries only while shown.

Assisted-by: Claude:claude-opus-5

* Read library selection categories from memory

The change category dialog and category updates read each selected
entry's categories from the loaded library instead of querying them one
at a time.

Assisted-by: Claude:claude-opus-5
2026-09-28 21:57:53 +06:00
AntsyLich 93cc075116 Tweak database table and view schema (#3805)
* Select explicit columns and name same-typed arguments

SQLDelight expands SELECT * using the table's column order and binds
mappers to it by position, so reordering columns would silently swap
mapper arguments. Queries list their columns instead. Calls whose
arguments share a type pass them by name, so swapping two still fails to
compile.

The generated code is unchanged apart from the SQL text.

* table - manga

Rebuild `mangas` as `manga` with a UNIQUE(source_id, remote_url) constraint,
`remote_`/`user_`/`state_` column prefixes and `_id` -> `id`. `remote_` is what the
source reported, `user_` is what the user set, `state_` is local bookkeeping that is
neither; `source_id` keeps its name because it identifies the source rather than
carrying its data.

Entries sharing a source and url are merged rather than dropped. The copies are ranked
with the ones in the library first, since that is the entry the user has, then by age.
The first one stays, takes each detail it is missing from the first copy that has it,
and absorbs the children of the rest. The duplicate chapters that repointing creates
are merged too, read state and history included.

The child tables keep pointing at the table this replaces until their own step rebuilds
them. Foreign keys are off for the whole migration, so nothing reads those clauses in
between, and copying every child table here just to rewrite one clause would double the
work the migration does. For the same reason the children resolve the surviving entry
while they are copied rather than being rewritten here first.

Merging entries is also what creates duplicate chapters and duplicate tracks, but each of
those is resolved in the step that rebuilds its own table, where the constraint that makes
the collision matter is introduced.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - chapter

Rebuild `chapters` as `chapter` with a UNIQUE(manga_id, remote_url) constraint,
`remote_`/`user_`/`state_` column prefixes and `_id` -> `id`.

The entries that merged in the previous step brought their chapters with them, so the
chapters move onto the surviving entry here and the duplicates that creates are merged:
the copy with reading history is kept, the oldest otherwise, and the read state and
history of the others are folded into it. It happens here rather than with the merge
because this is where the constraint that rejects the duplicates is introduced.

`chapters_manga_id_index`, `chapters_unread_by_manga_index` and `idx_chapters_url` go away:
the new unique constraint indexes `manga_id`, and the only url-only lookup has no callers.
The unread index served the updates view's read filter, but that view sorts by fetch date
and stops at a limit, which an index on `state_date_fetch` serves better with or without the
read filter, so that index takes its place. A second, partial one covers only the unread
chapters' fetch dates: most chapters in a library are read, so finding the unread ones
through the full index means walking nearly all of it.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - category

Rebuild `categories` as `category` with `_id` -> `id` and `sort` -> `order`, the name it
already had everywhere above the database. `order` gains a UNIQUE constraint, since two
categories sharing a position was never meaningful.

`sort` was never unique, and reordering wrote colliding values often enough that it cannot
simply be copied under the constraint. The migration renumbers the rows from 0 in the order
they were displayed, ties broken by id, and keeps the system category at -1.

`mangas_categories` is rebuilt in the migration only to point its foreign key at the new
table; it gets its real rebuild in its own step. Queries are only retargeted at the new
names; using the new structure is left to later changes.

Assisted-by: Claude:claude-opus-5

* table - manga_category

Rebuild `mangas_categories` as `manga_category`. The surrogate `_id` went unused, so the
pair becomes the identity with a UNIQUE(manga_id, category_id) constraint; duplicate
memberships were never meaningful. `idx_mangas_categories_manga_id` goes away because the
unique constraint already indexes `manga_id` as its leading column.

Entries merged in the manga step bring their memberships along. When the merged entry is
in the library, only the copies that were in it decide its categories, since a copy
outside the library can still hold the categories it had before it was removed.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - history

Rebuild `history` with `_id` -> `id`, `last_read` -> `read_at` and `time_read` ->
`read_duration`; the old names read as booleans rather than the timestamp and duration
they hold. `history_history_chapter_id_index` goes away because `chapter_id` is already
UNIQUE and therefore indexed.

A row also names the entry it belongs to now, not just the chapter. Reaching the entry
meant joining through the chapter every time, and the entry is the thing history is read
by. It is filled from the chapter it hangs off, and keyed to both: the entry first, then
the chapter. A row whose chapter is already gone cannot name an entry and is dropped,
which is what the foreign key would do to it once it is enforced again.

The table is the only one here that keeps its name, so the original is renamed aside and
the new one built under the real name. Building it beside the original and renaming it
into place makes SQLite rewrite the stored definition, which would leave this table
spelled differently from the same table on a fresh install.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - excluded_scanlator

Rebuild `excluded_scanlators` as `excluded_scanlator`, singular like the other tables.
The pair becomes the identity with a UNIQUE(manga_id, scanlator) constraint; excluding
the same scanlator twice for one entry was never meaningful.
`excluded_scanlators_manga_id_index` goes away because the unique constraint already
indexes `manga_id` as its leading column.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - manga_track

Rebuild `manga_sync` as `manga_track`: the table holds tracker entries, and nothing about
it is a sync. `_id` -> `id` and `sync_id` -> `tracker_id`, which is what it identifies.
`idx_manga_sync_manga_id` goes away because UNIQUE(manga_id, tracker_id) already indexes
`manga_id` as its leading column.

The constraint drops ON CONFLICT REPLACE and aborts like every other one. Replacing
deleted the row and inserted a new one under a new id, which lost anything keyed to the
old id, such as a queued tracking update, and it hid any other write that collided. The
insert now says what a conflict means: an existing track for the entry and tracker is
updated in place.

When entries merge, two rows for the same tracker can end up pointing at the survivor, so
the row to keep is chosen first: one on a copy in the library, then the one furthest read,
then the one on the copy ranked first, then the oldest. A track is taken whole rather than
merged field by field, because two rows on the same tracker can point at different remote
entries.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* table - source

Rebuild `sources` as `source` with `_id` -> `id` and `lang` -> `language`, spelled out
like every other column here.

Queries are only retargeted at the new names; using the new structure is left to later
changes.

Assisted-by: Claude:claude-opus-5

* view - updates

The view is the Kotlin-facing shape of a row, so its columns are named the way the
callers read them; `last_page_read` and `datefetch` were the only two that weren't.

The migration builds every view once at its end, so this updates that definition rather
than dropping and recreating it again. Queries are only retargeted at the new names;
using the new structure is left to later changes.

Assisted-by: Claude:claude-opus-5

* view - library

Bound the chapter and category aggregates to the library. Both subqueries group over
every chapter and every category membership in the database, and only the outer query
drops the entries that are not in the library, so an entry that was merely browsed still
pays for its chapters every time the library is read. The entries the view keeps are the
only ones whose aggregates it uses, so the same filter belongs inside them, where
`manga_user_favorite_at_index` can serve it.

The view returns the same rows either way. On a 560k chapter database holding 400 entries
among 4400, reading the library goes from ~156ms to ~23ms.

Assisted-by: Claude:claude-opus-5

* view - history

The view is the Kotlin-facing shape of a row, so its columns are named the way the
callers read them: `source` becomes `sourceId` and `state_cover_last_modified` loses the
prefix it only carried because the column it reads has one.

The migration builds every view once at its end, so this updates that definition rather
than dropping and recreating it again. Queries are only retargeted at the new names;
using the new structure is left to later changes.

Assisted-by: Claude:claude-opus-5
2026-09-28 21:44:11 +06:00
AntsyLich 9b5e0ae735 Show an extension's store and install failures in the extension list (#3955) 2026-09-28 08:55:09 +06:00
AntsyLich 09edf7dc8d Keep an installed extension's store current as stores change
Extensions only reload when the set of signing keys changes, so one could keep a store that was removed while another with the same key stayed, or a store's old name. The manager now reassigns each installed extension's store whenever the stores change.
2026-09-28 00:52:24 +06:00
AntsyLich 4f33486aa4 Clean up extension update checking
ExtensionApi only wrapped the store repository and the launch check, which matched updates again on its own. The manager now fetches listings and checks for updates itself with the statuses it already keeps, so the check covers not-loaded extensions and keeps the Extensions tab current from launch. Statuses are derived in one place, and the extension list is split before it's sorted instead of sorted twice.
2026-09-28 00:48:03 +06:00
AntsyLich 2d86a70ce2 Offer updates for extensions that aren't loaded
Extensions that aren't loaded now get updates like loaded ones, which is often what gets them loaded again. They're counted, notified and listed with the other pending updates. Every installed extension, loaded or not, records the added store whose key signed it when it loads.
2026-09-28 00:46:54 +06:00
AntsyLich 0938411053 Install and update extensions only from a store whose signing key matches
Updates come only from stores whose key the installed extension is signed with, the highest version among them. A downloaded apk must be signed with its store's key before it's installed.
2026-09-27 23:41:21 +06:00