Commit Graph
8075 Commits
Author SHA1 Message Date
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
Mend Renovate f35bc341ae Update dependency com.cheonjaeung.compose.grid:grid to v2.9.0 (#4041) 2026-10-04 12:06:42 +06:00
Mend Renovate a7a8cc8343 Update dependency com.mohamedrejeb.richeditor:richeditor-compose to v1.2.1 (#4030) 2026-10-04 11:55:19 +06:00
Chopper 4c88f02646 Fix race condition when updating search items in SearchViewModel (#4037) 2026-10-03 09:11:04 +00:00
Chopper a4780e4edb Use Dispatchers.IO.limitedParallelism in SearchViewModel to prevent thread leak (#4036) 2026-10-03 07:33:20 +00: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
ChopperandAntsyLich f2464a1ba6 Add an option to disable the MD5 suffix in chapter names (#3966)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-10-02 03:31:38 +06:00
w 98ba693054 Bump webgpuviewer (#4029)
* Add crop for long strip

* Don't save all animation frames in memory

* Fix navigation

* Update libs.versions.toml
2026-10-02 03:30:24 +06:00
AntsyLich 972c9d43da Bump versionCode to 33 2026-10-01 01:17:41 +06:00
Mend Renovate 866d045c7a Update gradle/actions action to v6.4.0 (#4024) 2026-09-29 03:33:54 +06:00
w 65d7110e1e Remove libvips based imagedecoder and add progressive rendering (#4023) 2026-09-29 03:11:45 +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
MajorTanyaandMitchell Syer ec614bb4c3 Use Apollo GraphQL for Trackers with GraphQL APIs (#3827)
* Add Apollo for GraphQL interactions

* Migrate AniList to use Apollo

* Migrate Shikimori to use Apollo

Not all Shikimori requests can be served by their GraphQL API (notably
they do not offer any mutations), so those remain as-is.

* Migrate Suwayomi to use Apollo

Includes leveraging some slightly newer Suwayomi features,
specifically the Double/Float filter fixes first pre-released in
2.1.1985 on 2025-10-26 and first fully released in 2.1.2100 on
2026-05-08.

Considering the tachiyomix 1.6 extension lib changes were added in
Suwayomi 2.3.2223 on 2026-06-30, I think it is fair to depend on such
sorta-recent behaviour.

* Fix temp migration names and leftovers

* Create ApolloResponse.dataOrElse extension

Because Apollo doesn't throw exceptions and instead just provides them
as response attributes, the current ApolloResponse.awaitSuccess() is
useless and will be removed shortly.

This new extension function should wrap the basic handling of an
ApolloResponse object neatly.

Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com>

* Use dataOrElse in AnilistApi.kt

* Use dataOrElse in ShikimoriApi.kt

* Use dataOrElse in SuwayomiApi.kt

* Delete obsolete ApolloCall.awaitSuccess

* Migrate Kitsu to use Apollo

Kitsu has some unfortunate edges in its GraphQL API (such as returning
nonstandard bodies which don't play well with Apollo) but this should
be an equivalent implementation.

* Extract a MangaFragment for AniList

This simplifies the queries and the converters (which can now delegate
to the MangaFragment.toTrackSearch() method internally and adjust the
track as needed).

Also move the toALUser converter to AnilistUtils.kt to keep all
converters together.

Added a scalar-mapper for AL's CountryCode type.

* Minor consistency cleanup of the queries

* Store list entry ID not title ID for Shikimori

* Extract common MangaFragment for Shikimori

Also map the ISO8601Date to a Kotlin String in the service config.

* Map Kitsu's ISO8601DateTime to kotlin.time.Instant

Utilises Apollo's own adapter-core library which provides the mapper.

Requires opting in to ExperimentalTime until the library resolves the
outdated annotation.

Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com>

* Replace Kitsu TODO comment

So much for STOPSHIP preventing me shipping this...

* Bump Apollo version to 5.1.0

* Bump apollo-adapters-core to 0.8.0

Freshly released just earlier.
This resolves having to opt in to ExperimentalTime.

* Make new utils internal to align with #3908

* Fix leftover issues from merged #3828

This PR restructured the AniList and Kitsu user objects.

* Bump apollo to 5.2.0

* Remove unused attributes from queries

* Allow custom exception handler in dataOrElse

Enables callers to manually handle specific non-200 responses.

Callers MUST close the `ApolloHttpException.body` attached to the
passed exception to avoid resource and socket leaks.

Defaults to current behaviour of (re)throwing the status code with our
HttpException.

Utilized in KitsuApi.kt to specially handle Kitsu sending a 500 error
response when deleting library IDs not actually in a user's library
and falling back to the "throw as HttpException" if it's any other
error response.

* Rethrow networking/IO exceptions

This helps to avoid masking networking problems as "This isn't in your
list" problems.

Currently, the app shows "Failed to connect to ..." when the remote
server is unreachable (e.g. downtime, DNS block, etc) because that's
the message provided by the ConnectException thrown by Android's
network stack.

This change restores that behaviour, approximately.

Not the most elegant solution but it _should_ avoid false-positive
toasts.

---------

Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com>
2026-09-28 10:34:26 +06:00
Mend Renovate 59fdf7e7fa Update dependency androidx.work:work-runtime to v2.12.0 (#4015) 2026-09-28 08:55:38 +06:00
Mend Renovate 7451c228e6 Update Gradle to v9.8.0 (#4016) 2026-09-28 08:55:25 +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
Mend Renovate 7f4c55176f Update dependency androidx.annotation:annotation to v1.11.0 (#4014) 2026-09-28 08:39:33 +06:00
Mend Renovate 9b4c3115db Update spotless to v8.10.3 (#4013) 2026-09-28 08:39:19 +06:00
Mend Renovate f835398094 Update metro to v1.4.5 (#4012) 2026-09-28 08:39:08 +06:00
Mend Renovate 3542143b40 Update dependency androidx.webkit:webkit to v1.17.1 (#4011) 2026-09-28 08:38:55 +06:00
Mend Renovate ba4c04fe74 Update dependency androidx.core:core to v1.19.1 (#4010) 2026-09-28 08:38:41 +06:00
Mend Renovate 087e38ad47 Update dependency androidx.compose:compose-bom-alpha to v2026.09.01 (#4009) 2026-09-28 08:38:29 +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
AntsyLich ed17ece4e3 Return an entry's categories in display order
An entry's categories came back in whatever order SQLite found them. They're now ordered by position, the same order the library and the category list show.
2026-09-27 23:10:36 +06:00
AntsyLich b74e010bfe Order updates by fetch date in the queries themselves
The updates queries relied on the view's ORDER BY surviving into the query that reads from it, which SQLite doesn't promise. Each now orders by fetch date itself.
2026-09-27 23:10:14 +06:00
AntsyLich 4b48a84ec9 Keep a stored track's remote entry when restoring
Restoring over an existing track took the backup's remote id and library id but kept the stored url and title, pairing one remote entry's id with another's details. The stored track now wins like the rest of an entry's stored data, and the backup only moves its last chapter read forward.
2026-09-27 23:09:32 +06:00
AntsyLich 9a77baedcc Update an existing track in place instead of replacing it
Upserting a track for an entry and tracker that already have one updates that row, instead of the table's REPLACE deleting it and inserting a new one under a new id. Anything keyed to the old id, such as a queued tracking update, keeps working. The query, repository methods and interactor are renamed from insert to upsert to say so.
2026-09-27 23:02:26 +06:00
AntsyLich 2b4b8472bb Open a requested chapter in the reader even if its scanlator is excluded by filter
The reader builds its chapter list with excluded scanlators filtered out, so a chapter opened directly (from a deep link or a notification) whose scanlator is excluded wasn't in the list and failed to open. The requested chapter is now added back when the filter removed it.
2026-09-27 22:56:42 +06:00
AntsyLich 553762fae8 Fix reading history being double counted
Restore groups a backup's history entries by chapter before comparing them with the stored row, so a backup carrying one entry per copy of a duplicated chapter no longer adds its reading time more than once or moves the last read backwards. A removed history entry keeps its 0 instead of becoming NULL, a tracker's start date skips removed history, and the reader clears a session's start time before saving it, so overlapping saves can't count it twice.
2026-09-27 22:55:41 +06:00
AntsyLich 8bfa495f34 Assign category positions inside the database
A new category's position is computed by its insert, one after the highest, so two categories created at once can't take the same one. Inserts take a NewCategory rather than a Category whose id and position would be ignored. Reordering places any category missing from the given order after the rest instead of leaving it at a stale position. Restore still creates missing categories all or none, in backup order, and the create dialog no longer accepts an empty name.
2026-09-27 22:53:09 +06:00
AntsyLich fc5592ff75 Don't bring back extension stores removed during a refresh
A store's refreshed details are written only if the store still exists when its fetch returns, so one removed while its index was loading stays removed. The unused get query becomes contains.
2026-09-27 22:44:15 +06:00
AntsyLich 0044cc73ec Fix stored stub sources not loading and being rewritten on every reload
Stub sources loaded from the database were written into a throwaway copy of the map, so they never reached the source manager. StubSource also becomes a data class, so a stored one compares equal to the same source again and isn't rewritten on every reload.
2026-09-27 22:43:48 +06:00
AntsyLich 6ee529c5aa Apply chapter sync in one transaction
A chapter sync's removals, inserts and updates go through one ChapterRepository.updateFromRemote call, applied atomically. A failure no longer leaves some of a sync's changes written and others missing. Restore folds duplicate chapters in a backup into one before writing, and the chapter deep link looks up the synced chapter afterwards rather than relying on the sync's return value.
2026-09-27 22:41:55 +06:00
AntsyLich 4df0e6b168 Keep category orders unique while reordering
Reordering first moves every category to a temporary negative position, then assigns the final ones, so two categories never share a position mid-update. Deleting a category no longer renumbers the system category.
2026-09-27 22:37:39 +06:00
AntsyLich 986f46c09a Merge manga favorite related fields into favorite_at
Migration 16 replaces favorite, date_added and favorite_modified_at with one nullable favorite_at. A favorite whose date_added is 0 falls back to favorite_modified_at. Manga.favorite and dateAdded become getters derived from it, and restore merges it by taking the earliest non-zero time.
2026-09-27 22:37:02 +06:00