Compare commits

..

85 Commits

Author SHA1 Message Date
Jobobby04 129841d5c2 SpotlessApply and up version code due to database migration 2025-05-11 20:23:10 -04:00
AntsyLich 24d2460697 Disable reader's 'Keep screen on' setting by default (#2095)
(cherry picked from commit f0de8f973b331ebad6e1844aea7864f97f237941)

# Conflicts:
#	CHANGELOG.md
2025-05-11 20:18:36 -04:00
AntsyLich ef3d9626c1 Add full predictive back support (#2085)
Co-authored-by: p
(cherry picked from commit c12bdbae8e7bc14da8966e45a3c450913e32129f)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
2025-05-11 20:18:11 -04:00
Mend Renovate 5c7b3c6c3b Update markdown to v0.34.0 (#2086)
(cherry picked from commit 33d407ee9c2196f25aaee0978b6808b9c393841e)
2025-05-11 20:10:50 -04:00
AntsyLich 6257888735 Update voyager to v1.1.0-beta03 (#2087)
(cherry picked from commit ef8c3ca119dfbdfcb0588bde080a7c6203133024)
2025-05-11 20:10:43 -04:00
FlaminSarge f5e714f794 Add advanced option to always update manga title from source (#1182)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8b45ef0e5d5d368e0925df9816ae423defaed4d9)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
2025-05-11 20:10:28 -04:00
AwkwardPeak7 3091f63504 Fix pressing Enter while searching also triggering navigation back on physical keyboards (#2077)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 86ebf5581598f28feab4090ac3bf627f54b511d7)

# Conflicts:
#	CHANGELOG.md
2025-05-11 20:00:59 -04:00
Mend Renovate 39755cccdc Update lifecycle.version to v2.9.0 (#2080)
(cherry picked from commit ddf282b10364fc71c5af4cfe77684a8ac0beaa5c)
2025-05-11 20:00:40 -04:00
Mend Renovate 9caf706ca3 Update sqlite to v2.5.1 (#2078)
(cherry picked from commit 744b809d458ba1bfee923f76493505768c261231)
2025-05-11 20:00:32 -04:00
Mend Renovate 6ba6a7c8d9 Update dependency androidx.compose:compose-bom to v2025.05.00 (#2079)
(cherry picked from commit cd2ce44efafbaf19e43ba342d417e4d553a8a0d7)
2025-05-11 20:00:26 -04:00
Mend Renovate 0a4a0e4c4c Update dependency com.android.tools.build:gradle to v8.10.0 (#2072)
(cherry picked from commit cae7c3dc588055dee5c9a99c331710e038b17794)
2025-05-11 20:00:19 -04:00
Mend Renovate b48d1e521a Update aboutlib.version to v12.1.2 (#2073)
(cherry picked from commit c0074402e7cc5917272459e7ebfb269e11af4c76)
2025-05-11 20:00:08 -04:00
AntsyLich 211d090a2d Add autofill support to tracker login dialog and update processing text (#2069)
(cherry picked from commit 7deeabe844d41d2b5e918ad747ddd548163c9fe3)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:59:44 -04:00
AntsyLich b6e5943e15 Fix downloader stopping after failing to create download directory of a manga (#2068)
(cherry picked from commit 536393a6d9941fac282f10b825aa611b91e1fcdb)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
2025-05-11 19:59:22 -04:00
AntsyLich 78f6a34339 Fix Pill not following the local text style
Closes #2009

(cherry picked from commit f8cb506137a3619f828dac94557b5448b2a7fa24)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:48:31 -04:00
AntsyLich de967ae149 Cleanup MarkdownRender
Co-authored-by: p
(cherry picked from commit 98230ed30f04fe754fd4bd407356c8c03d8d8719)
2025-05-11 19:48:04 -04:00
Mend Renovate 4d075ff190 Update dependency androidx.compose:compose-bom to v2025.04.01 (#2040)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit d721a4321bdc6fafdd32e7bfd451b61b2bdd66b7)
2025-05-11 19:47:58 -04:00
Mend Renovate 076e2961c6 Update aboutlib.version to v12.1.0 (#2052)
(cherry picked from commit 1ac4b72cfe09be10cbde45633fbd84ffd703fc70)
2025-05-11 19:47:52 -04:00
Mend Renovate 7149de1bc3 Update dependency io.mockk:mockk to v1.14.2 (#2057)
(cherry picked from commit 9331f2b93f907bf3f7c95eb850b5befce90b68c6)
2025-05-11 19:47:42 -04:00
Mend Renovate 091f2f583a Update dependency org.jsoup:jsoup to v1.20.1 (#2058)
(cherry picked from commit 99c2a999735e04d026b937b953cdd5f19b2e7b1f)
2025-05-11 19:47:36 -04:00
Mend Renovate 1c0ef2ca98 Update aboutlib.version (#2046)
(cherry picked from commit 001716e34b0c533cfd0318be5048d32bc107931b)
2025-05-11 19:47:29 -04:00
NarwhalHorns 2a845bd7b5 Fix empty layout not appearing in browse source screen in some cases (#2043)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 37e19edf8a5f6a15a95f160390cbcf22d8133380)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt
2025-05-11 19:47:22 -04:00
AntsyLich afe326006f Switch default user agent to Android Chrome (#2048)
(cherry picked from commit 8b7f35598833917c89f8ae53cca10578fd880d67)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:44:45 -04:00
Mend Renovate 4b80154b09 Update dependency com.google.firebase:firebase-bom to v33.13.0 (#2047)
(cherry picked from commit 0b777336739b6f91ebfe45772c2f139d4e60c555)
2025-05-11 19:44:30 -04:00
Mend Renovate d6b230b8f1 Update dependency androidx.work:work-runtime to v2.10.1 (#2041)
(cherry picked from commit 9be558d6c0e0e81c1f37fc3d01b19872879f2daa)
2025-05-11 19:44:23 -04:00
Mend Renovate d02a2cbd29 Update dependency gradle to v8.14 (#2049)
(cherry picked from commit 0c8c5dbba6bf60fbc4e3e4dad912e2be8ce25a79)
2025-05-11 19:44:17 -04:00
AntsyLich 17d225b0d9 Fix crash when trying use source sort filter without a pre-selection (#2036)
(cherry picked from commit 1c982c2a01c1bba5ec4a955c9bf61cb346c752e7)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:44:10 -04:00
Mend Renovate 6cbbaccaf4 Update dependency com.android.tools.build:gradle to v8.9.2 (#2033)
(cherry picked from commit eeab61fc94e1a9486eba42fd79a8169473ab6fde)
2025-05-11 19:43:51 -04:00
Mend Renovate 94cc4027c2 Update aboutlib.version to v12 (major) (#2016)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit a036407c75d83ef0ba5350cb1825b615361316c3)
2025-05-11 19:43:44 -04:00
AntsyLich 03ae6ed2b0 Update dependency com.mohamedrejeb.richeditor:richeditor-compose to v1.0.0-rc11
(cherry picked from commit 615d93f780b415e505fb2159f4cbdf4694749f82)
2025-05-11 19:43:35 -04:00
AntsyLich fa8c232a69 Fix content under source browse screen top appbar is interactable (#2026)
(cherry picked from commit 9750c1e4bd6b931e71b7b348abbe2638a8cf317b)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:43:27 -04:00
Secozzi 0386ce998a Update markdown to 0.33.0 and tweak visuals (#2024)
- Update markdown to 0.33.0
- Use github flavour for github changelog
- Fix bullet list alignment

(cherry picked from commit e2915a1f69340cad515962de8a0b9d11ecff8d42)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:43:12 -04:00
AwkwardPeak7 273f73e9a2 Remove Okhttp networking from WebView Screen (#2020)
(cherry picked from commit df2b4c754bab9dd96fe2199b9f6df62d87b7175e)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:42:33 -04:00
KokaKiwi 5e20e54649 Fix reader not updating progress (#2007)
The condition for updating progress is wrong since fefa8f84982b537ca930438f7976087844d5bb9c

(cherry picked from commit 6632a122288cc9733844c8dce1ee51b520c0a32e)
2025-05-11 19:42:06 -04:00
Joseph Madamba b8c3f9dcce Update Facebook and Reddit icon (#1994)
(cherry picked from commit 0cb1925cf158155665f3173bccb93f39d84b71e0)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:59 -04:00
ArthurKun 802b6508fa Replace Modifier.composed with Composable Modifier (#1959)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit a31b3b7bbf2c5164baf76ac4b36f1d27c5d43135)
2025-05-11 19:41:36 -04:00
AwkwardPeak7 b6409b05e7 Include source headers when opening failed images from reader (#2004)
(cherry picked from commit fea85241afac5a849aa418d01710f5cdc0c25b54)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:27 -04:00
Secozzi 129f355b9c Use simpler markdown flavour in manga description (#2000)
(cherry picked from commit e273a26c9b7f0a9dd9f8847cfc65e69453fa5905)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:41:01 -04:00
AwkwardPeak7 9ffacb80e3 Fix duplicate requests in WebView due to empty reasonPhrase (#2003)
(cherry picked from commit 818e6931c6bc89e0bb111e77418542a88f8db37c)

# Conflicts:
#	CHANGELOG.md
2025-05-11 19:40:27 -04:00
AwkwardPeak7 85726db45d Add option to keep read manga when clearing database (#1979)
(cherry picked from commit ecc6ede0815a89b7f8288e47c607c57bacc47e71)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
2025-05-11 19:40:06 -04:00
AwkwardPeak7 746b1b051c Surface image loading error in Reader (#1981)
(cherry picked from commit fefa8f84982b537ca930438f7976087844d5bb9c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
2025-05-11 19:36:53 -04:00
AwkwardPeak7 59887eed80 Change Page.State to sealed interface (#1988)
(cherry picked from commit f1e2efcb37e2c623b769e979fa1c7e9e5ad7117d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ArchivePageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt
2025-05-11 19:35:07 -04:00
Mend Renovate b8267f1fef Update dependency androidx.core:core-ktx to v1.16.0 (#1990)
(cherry picked from commit 180318f57d82529c0040a2d310a679a493d2b9f3)
2025-05-11 19:12:53 -04:00
Mend Renovate 8c62bb6d6d Update plugin org.gradle.toolchains.foojay-resolver-convention to v0.10.0 (#1992)
(cherry picked from commit ed749de8066ac056bc0caa505739f1b7e45dea48)
2025-05-11 19:12:46 -04:00
Mend Renovate 751e04b87f Update markdown to v0.33.0-rc01 (#1999)
(cherry picked from commit bb33b0029ef630a609dd160ed278d5e8274f316d)
2025-05-11 19:12:40 -04:00
Mend Renovate 9f0161ed70 Update dependency androidx.compose:compose-bom to v2025.04.00 (#1989)
(cherry picked from commit 3a19e449b13c078d7c0b5762f0ba84a162cc7f71)
2025-05-11 19:12:33 -04:00
Mend Renovate 7b2c341386 Update dependency com.squareup.okio:okio to v3.11.0 (#1991)
(cherry picked from commit 818edf2776fd7706cd1a829a6bdc963a436f06d6)
2025-05-11 19:12:22 -04:00
Mend Renovate c8b29ecf1c Update dependency io.mockk:mockk to v1.14.0 (#1987)
(cherry picked from commit 47d2646751d40a155724f658ba6461e8a2d57aad)
2025-05-11 19:12:15 -04:00
Mend Renovate c30381c6ec Update dependency androidx.sqlite:sqlite-framework to v2.5.0 (#1986)
(cherry picked from commit a1a7d67afb16cc206f1d6c6d47da52b7705a417d)
2025-05-11 19:12:08 -04:00
Mend Renovate f489531543 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v7.0.3 (#1977)
(cherry picked from commit 2090a380e0e9ab4f74fd2e5e74e6c2807e96f23d)
2025-05-11 19:12:01 -04:00
Mend Renovate 4bbe795626 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2 (#1978)
(cherry picked from commit 8e5cfe9d0acf854c9387ed3405f0fe3cc9140733)
2025-05-11 19:11:54 -04:00
Cuong-Tran 8aa3dca95f Fix navigation issue after migrating a duplicated entry from History tab
(cherry picked from commit d9c4b56336c21db96a835630a48c46ee7a480342)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
2025-05-11 19:11:40 -04:00
NarwhalHorns 5e0f730159 Display total chapters on duplicates list items (#1963)
(cherry picked from commit 12abd9938b7c235d6a1c02391624703476c1f339)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
#	data/src/main/java/tachiyomi/data/manga/MangaMapper.kt
2025-05-11 19:03:47 -04:00
Mend Renovate f1aed0d8b9 Update dependency androidx.compose:compose-bom to v2025.03.01 (#1927)
(cherry picked from commit c1225a5ef96f5f77bd337e0481935cbef75cc711)
2025-05-11 18:57:57 -04:00
AntsyLich a3465c31c9 Update non-library manga data when browsing (#1967)
(cherry picked from commit a594ad392d4793f3a5cb2b709d29b2feab6120d3)

# Conflicts:
#	CHANGELOG.md
2025-05-11 18:57:50 -04:00
Mend Renovate 053c48613b Update xml.serialization.version to v0.91.0 (#1956)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 80de0328190bd3adac8e034c420e6a91d3d7cfc9)
2025-05-11 18:55:30 -04:00
NarwhalHorns 615adc567b Display all similarly named duplicates in duplicate manga dialogue (#1861)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 0d35b6fdafbf5451a2743ea9bcfc735bf49374a7)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
2025-05-11 18:14:26 -04:00
AntsyLich b0f645d906 Deduplicate entries when browsing (#1957)
(cherry picked from commit f81da3dcce9afba883b6a3accdd3bf4ea21cfa81)

# Conflicts:
#	CHANGELOG.md
#	data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt
2025-05-11 18:05:21 -04:00
Mend Renovate 023c78d0e8 Update serialization.version to v1.8.1 (#1953)
(cherry picked from commit 2ce9fa0271c16449475adb07eb6338a497e11e3c)
2025-05-11 17:58:51 -04:00
AntsyLich 824550175a Remove feature flag from Nord theme (#1951)
(cherry picked from commit 5d2110f3fb1aa6b15f62af0dcd3378cfbe475b7a)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt
2025-05-11 17:32:37 -04:00
Secozzi ad53c0de83 Add markdown support for manga descriptions (#1948)
(cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1)

# Conflicts:
#	CHANGELOG.md
#	gradle/libs.versions.toml
2025-05-11 17:31:41 -04:00
AntsyLich c8039739d5 Significantly improve browsing speed (near instantaneous) (#1946)
(cherry picked from commit c8ffabc84a096207c1997ab69fc86176f3b53f00)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
#	data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt
#	data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt
#	data/src/main/sqldelight/tachiyomi/data/mangas.sq
#	domain/src/main/java/tachiyomi/domain/manga/interactor/NetworkToLocalManga.kt
#	domain/src/main/java/tachiyomi/domain/manga/repository/MangaRepository.kt
#	domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt
2025-05-11 17:24:33 -04:00
Bartu Özen 26674136e6 Fix app bar action tooltips blocking clicks (#1928)
(cherry picked from commit 77e79233ab054d16bb5dc04a040d0d86a326136f)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:50:49 -04:00
AntsyLich 9972fa1053 Fix mark existing duplicate read chapters as read option not working in some cases (#1944)
(cherry picked from commit 8a21148578af3c1538e9ab2b1fe5bdf05b4e35c9)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2025-05-11 16:50:02 -04:00
AntsyLich ae3f974d8c Fix user notes not restoring when manga doesn't exist in DB (#1945)
(cherry picked from commit e91db86faef8d6b17961a1b73fbf07f0d2c8975d)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:44:09 -04:00
Mend Renovate 027f179a4b Update kotlin monorepo to v2.1.20 (#1883)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 556290f2d35f739bb4bddc012739acf10b92708d)
2025-05-11 16:43:30 -04:00
Mend Renovate e80cb1795e Update dependency com.android.tools.build:gradle to v8.9.1 (#1913)
(cherry picked from commit 8b947919acde9932808e666b8bb6a2df9613a67f)
2025-05-11 16:43:22 -04:00
Mend Renovate 66fe599498 Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.4 (#1926)
(cherry picked from commit b62a9b40eb8ecb4c0c9c861d66c9afc427bc6bbe)
2025-05-11 16:43:15 -04:00
AntsyLich c9e6e321b3 Update editor config for 'sq' and 'sqm' file [skip ci]
(cherry picked from commit a6b532ee57d24e1ad83f1daea415cad1f313b49c)
2025-05-11 16:43:02 -04:00
kunet fb3c996904 Add user manga notes (#428)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 8fbe630308b962043c7b59422878c94f80156e9f)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
#	data/src/main/sqldelight/tachiyomi/migrations/5.sqm
#	domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt
2025-05-11 16:42:33 -04:00
perokhe 70b25825ec Fix page number not appearing when opening chapter (#1936)
(cherry picked from commit 132d77aa9947f891f90f1afcdcb24e20ce515438)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:26:43 -04:00
Cuong-Tran 290e8f5a1e Fix benchmark build (#1938)
(cherry picked from commit b00bbe91beb942f2ac18765be6c78b6f318cc66d)
2025-05-11 16:25:36 -04:00
Jayman Rana f6b1440bf2 Fix backup sharing from notifications not working when app is in background (#1929)
(cherry picked from commit 3e5d3d099fed5feb6a6807196bea5fed72973fe9)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:25:27 -04:00
perokhe 77a4919656 Fix next chapter button occasionally jumping to the last page of the current chapter (#1920)
(cherry picked from commit 941dde341eb11703eadae543f351c9284617541c)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
2025-05-11 16:25:14 -04:00
Ian Hunter 84d901b8a3 Add more Kaomoji for empty/error screens (#1909)
(cherry picked from commit d4aaf6521e86e8509d3971854c46b8520cef7f59)

# Conflicts:
#	CHANGELOG.md
2025-05-11 16:22:24 -04:00
Mend Renovate d27ed2580f Update dependency com.google.firebase:firebase-bom to v33.11.0 (#1890)
(cherry picked from commit f7046a503bea421a0310f8d2064888aea0a07d11)
2025-05-11 16:22:08 -04:00
MajorTanya 87ea971be0 Fix Bangumi search including novels (#1885)
(cherry picked from commit 953c4e7bc056ed8b9eebe1b111677a4616c4d694)

# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/dto/BGMSearch.kt
2025-05-11 16:22:01 -04:00
Weblate (bot) 91ea70b335 Translations update from Hosted Weblate (#1877)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: scb261 <scb261261@gmail.com>
(cherry picked from commit 1d6dc1e8b0de08af1370c04eff480e4555095c55)
2025-05-11 16:21:27 -04:00
AntsyLich 2e94e152c2 Use current time as build time for preview builds (#1876)
(cherry picked from commit 935f1fcf3f8e4f9da4774d932b65ae77b44cc773)

# Conflicts:
#	app/build.gradle.kts
2025-05-11 16:21:22 -04:00
Weblate (bot) eece46fb0f Translations update from Hosted Weblate (#1550)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/he/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/be/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bg/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/km/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/mr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ta/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uz/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Abay Emes <abayemes@gmail.com>
Co-authored-by: Acelith <joel.jon@moix.me>
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Champ0999 <il.migliore0999@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Doministo <doministo@seznam.cz>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Harshit Prajapati <harshitprajapati7666@gmail.com>
Co-authored-by: Hasanur Rahman Biplob <hrbiplob10@gmail.com>
Co-authored-by: Horace Johnson <horacejohnson99@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Jakub Fabijan <jakubfabijan@tuta.io>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: Koanrade <konrad.nowicki91@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: MD_Abdulla072 <md.abdullacse20@gmail.com>
Co-authored-by: Matyáš Caras <matyas@caras.wtf>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mochammad Nopal Attasya <meleboy22@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: NormalRandomPeople <normal.scribe833@silomails.com>
Co-authored-by: Pecs1 <mynameisnoname897@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Piyoka Smith <piyoka5697@ahaks.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Rom Savidor <romsavidor@gmail.com>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Shiratori <kuromaruhatake@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
Co-authored-by: Sixten Lund <arbitraryindices@users.noreply.hosted.weblate.org>
Co-authored-by: Sorawit Jannareubate <moszaduck007@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: TheKingTermux <50316075+TheKingTermux@users.noreply.github.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: dianisaac <muhandreop@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: kevans <albapazpi@gmail.com>
Co-authored-by: staxhinho <staxhinho@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
(cherry picked from commit b3726572381abfa1eaaf31cf7f3b685b390f60bf)

# Conflicts:
#	i18n/src/commonMain/moko-resources/de/strings.xml
#	i18n/src/commonMain/moko-resources/kk/strings.xml
#	i18n/src/commonMain/moko-resources/lt/strings.xml
#	i18n/src/commonMain/moko-resources/ru/strings.xml
2025-05-11 16:19:26 -04:00
AntsyLich 34736bc26e For release builds use last commit time as build time (#1873)
(cherry picked from commit dae7d179662ff6d6654e7c10e57f1aeeaf579de8)

# Conflicts:
#	app/build.gradle.kts
2025-05-11 16:17:52 -04:00
renovate[bot] 82cf385f9d Update dependency net.zetetic:sqlcipher-android to v4.8.0 (#1429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:14:08 -04:00
renovate[bot] 682dea2fb1 Update koin to v4.0.4 (#1428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:12:00 -04:00
renovate[bot] c10588d183 Update tachiyomiorg/issue-moderator-action action to v2.6.1 (#1388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:11:44 -04:00
Jobobby04 6db1637770 Fix library flags test 2025-05-11 14:57:14 -04:00
211 changed files with 5014 additions and 1711 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.xml] [*.{xml,sq,sqm}]
indent_size = 4 indent_size = 4
# noinspection EditorConfigKeyCorrectness # noinspection EditorConfigKeyCorrectness
+11 -2
View File
@@ -14,8 +14,14 @@ jobs:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Set up JDK
uses: gradle/actions/wrapper-validation@v4 uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@v4
- name: Create Tag - name: Create Tag
run: | run: |
@@ -28,3 +34,6 @@ jobs:
-H 'Accept: application/vnd.github.everest-preview+json' \ -H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.ACCESS_TOKEN }} \ -u ${{ secrets.ACCESS_TOKEN }} \
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' --data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
- name: Run unit tests
run: ./gradlew testDebugUnitTest testDevDebugUnitTest
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Moderate issues - name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v2.6.0 uses: tachiyomiorg/issue-moderator-action@v2.6.1
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
duplicate-label: Duplicate duplicate-label: Duplicate
+7 -12
View File
@@ -31,12 +31,12 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 73 versionCode = 74
versionName = "1.12.0" versionName = "1.12.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"") buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false") buildConfigField("boolean", "INCLUDE_UPDATER", "false")
ndk { ndk {
@@ -71,6 +71,8 @@ android {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
} }
create("benchmark") { create("benchmark") {
initWith(getByName("release")) initWith(getByName("release"))
@@ -237,7 +239,7 @@ dependencies {
implementation(libs.preferencektx) implementation(libs.preferencektx)
// Dependency injection // Dependency injection
implementation(libs.injekt.core) implementation(libs.injekt)
// Image loading // Image loading
implementation(platform(libs.coil.bom)) implementation(platform(libs.coil.bom))
@@ -255,7 +257,7 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.bundles.richtext) implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose) implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion) implementation(libs.compose.materialmotion)
@@ -263,6 +265,7 @@ dependencies {
implementation(libs.compose.webview) implementation(libs.compose.webview)
implementation(libs.compose.grid) implementation(libs.compose.grid)
implementation(libs.reorderable) implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)
@@ -313,14 +316,6 @@ dependencies {
} }
androidComponents { androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
listOf("default" to "dev"),
)
}
}
onVariants(selector().withFlavor("default" to "standard")) { onVariants(selector().withFlavor("default" to "standard")) {
// Only excluding in standard flavor because this breaks // Only excluding in standard flavor because this breaks
// Layout Inspector's Compose tree // Layout Inspector's Compose tree
@@ -82,6 +82,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.domain.release.interactor.GetApplicationRelease import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.service.ReleaseService import tachiyomi.domain.release.service.ReleaseService
@@ -128,6 +129,7 @@ class DomainModule : InjektModule {
addFactory { SetMangaViewerFlags(get()) } addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) } addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get(), get()) } addFactory { UpdateManga(get(), get()) }
addFactory { UpdateMangaNotes(get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addFactory { GetExcludedScanlators(get()) } addFactory { GetExcludedScanlators(get()) }
addFactory { SetExcludedScanlators(get()) } addFactory { SetExcludedScanlators(get()) }
@@ -4,6 +4,7 @@ import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
@@ -32,9 +33,8 @@ class UpdateManga(
remoteManga: SManga, remoteManga: SManga,
manualFetch: Boolean, manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(), coverCache: CoverCache = Injekt.get(),
// SY --> libraryPreferences: LibraryPreferences = Injekt.get(),
downloadManager: DownloadManager = Injekt.get(), downloadManager: DownloadManager = Injekt.get(),
// SY <--
): Boolean { ): Boolean {
val remoteTitle = try { val remoteTitle = try {
remoteManga.title remoteManga.title
@@ -42,14 +42,13 @@ class UpdateManga(
"" ""
} }
// SY --> // if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
val title = if (remoteTitle.isNotBlank() && localManga.ogTitle != remoteTitle) { val title =
downloadManager.renameMangaDir(localManga.ogTitle, remoteTitle, localManga.source) if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
remoteTitle remoteTitle
} else { } else {
null null
} }
// SY <--
val coverLastModified = val coverLastModified =
when { when {
@@ -69,7 +68,7 @@ class UpdateManga(
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
return mangaRepository.update( val success = mangaRepository.update(
MangaUpdate( MangaUpdate(
id = localManga.id, id = localManga.id,
title = title, title = title,
@@ -84,6 +83,10 @@ class UpdateManga(
initialized = true, initialized = true,
), ),
) )
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
} }
suspend fun awaitUpdateFetchInterval( suspend fun awaitUpdateFetchInterval(
@@ -76,24 +76,6 @@ fun Manga.copyFrom(other: SManga): Manga {
) )
} }
fun SManga.toDomainManga(sourceId: Long): Manga {
return Manga.create().copy(
url = url,
// SY -->
ogTitle = title,
ogArtist = artist,
ogAuthor = author,
ogThumbnailUrl = thumbnail_url,
ogDescription = description,
ogGenre = getGenres(),
ogStatus = status.toLong(),
// SY <--
updateStrategy = update_strategy,
initialized = initialized,
source = sourceId,
)
}
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists() return coverCache.getCustomCoverFile(id).exists()
} }
@@ -1,8 +1,6 @@
package eu.kanade.domain.ui.model package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) { enum class AppTheme(val titleRes: StringResource?) {
@@ -11,9 +9,7 @@ enum class AppTheme(val titleRes: StringResource?) {
GREEN_APPLE(MR.strings.theme_greenapple), GREEN_APPLE(MR.strings.theme_greenapple),
LAVENDER(MR.strings.theme_lavender), LAVENDER(MR.strings.theme_lavender),
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
NORD(MR.strings.theme_nord),
// TODO: re-enable for preview
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako), TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise), TEALTURQUOISE(MR.strings.theme_tealturquoise),
@@ -82,10 +82,18 @@ fun BrowseSourceContent(
} }
} }
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(Modifier.padding(contentPadding))
return
}
if (mangaList.itemCount == 0) {
EmptyScreen( EmptyScreen(
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),
message = getErrorMessage(errorState), message = when (errorState) {
is LoadState.Error -> getErrorMessage(errorState)
else -> stringResource(MR.strings.no_results_found)
},
actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) { actions = if (source is LocalSource /* SY --> */ && onLocalSourceHelpClick != null /* SY <-- */) {
persistentListOf( persistentListOf(
EmptyScreenAction( EmptyScreenAction(
@@ -104,7 +112,7 @@ fun BrowseSourceContent(
// SY --> // SY -->
if (onWebViewClick != null) { if (onWebViewClick != null) {
EmptyScreenAction( EmptyScreenAction(
MR.strings.action_open_in_web_view, stringRes = MR.strings.action_open_in_web_view,
icon = Icons.Outlined.Public, icon = Icons.Outlined.Public,
onClick = onWebViewClick, onClick = onWebViewClick,
) )
@@ -113,7 +121,7 @@ fun BrowseSourceContent(
}, },
if (onHelpClick != null) { if (onHelpClick != null) {
EmptyScreenAction( EmptyScreenAction(
MR.strings.label_help, stringRes = MR.strings.label_help,
icon = Icons.AutoMirrored.Outlined.HelpOutline, icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = onHelpClick, onClick = onHelpClick,
) )
@@ -128,13 +136,6 @@ fun BrowseSourceContent(
return return
} }
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
LoadingScreen(
modifier = Modifier.padding(contentPadding),
)
return
}
// SY --> // SY -->
if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) { if (source?.isEhBasedSource() == true && ehentaiBrowseDisplayMode) {
BrowseSourceEHentaiList( BrowseSourceEHentaiList(
@@ -1,10 +1,9 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.activity.compose.BackHandler import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@@ -28,20 +27,14 @@ fun NavigatorAdaptiveSheet(
screen = screen, screen = screen,
content = { sheetNavigator -> content = { sheetNavigator ->
AdaptiveSheet( AdaptiveSheet(
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
) { ) {
ScreenTransition( ScreenTransition(
navigator = sheetNavigator, navigator = sheetNavigator,
transition = { enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith exitTransition = { fadeOut(animationSpec = tween(90)) },
fadeOut(animationSpec = tween(90)) sizeTransform = { SizeTransform() },
},
)
BackHandler(
enabled = sheetNavigator.size > 1,
onBack = sheetNavigator::pop,
) )
} }
@@ -79,10 +72,10 @@ fun AdaptiveSheet(
properties = dialogProperties, properties = dialogProperties,
) { ) {
AdaptiveSheetImpl( AdaptiveSheetImpl(
modifier = modifier,
isTabletUi = isTabletUi, isTabletUi = isTabletUi,
enableSwipeDismiss = enableSwipeDismiss, enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier,
) { ) {
content() content()
} }
@@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -201,6 +202,7 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = it.onClick, onClick = it.onClick,
@@ -225,6 +227,7 @@ fun AppBarActions(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { showMenu = !showMenu }, onClick = { showMenu = !showMenu },
@@ -289,6 +292,7 @@ fun SearchToolbar(
onSearch(searchQuery) onSearch(searchQuery)
focusManager.clearFocus() focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
focusManager.moveFocus(FocusDirection.Next)
} }
BasicTextField( BasicTextField(
@@ -352,6 +356,7 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
@@ -371,6 +376,7 @@ fun SearchToolbar(
} }
}, },
state = rememberTooltipState(), state = rememberTooltipState(),
focusable = false,
) { ) {
IconButton( IconButton(
onClick = { onClick = {
@@ -1,44 +1,95 @@
package eu.kanade.presentation.manga package eu.kanade.presentation.manga
import androidx.compose.foundation.clickable import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.PersonOutline
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.SwapVert import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastMaxOfOrNull
import coil3.request.ImageRequest
import coil3.request.crossfade
import eu.kanade.presentation.components.AdaptiveSheet import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Badge
import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun DuplicateMangaDialog( fun DuplicateMangaDialog(
duplicates: List<MangaWithChapterCount>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onOpenManga: () -> Unit, onOpenManga: (manga: Manga) -> Unit,
onMigrate: () -> Unit, onMigrate: (manga: Manga) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val sourceManager = remember { Injekt.get<SourceManager>() }
val minHeight = LocalPreferenceMinHeight.current val minHeight = LocalPreferenceMinHeight.current
val horizontalPadding = PaddingValues(horizontal = TabbedDialogPaddings.Horizontal)
val horizontalPaddingModifier = Modifier.padding(horizontalPadding)
AdaptiveSheet( AdaptiveSheet(
modifier = modifier, modifier = modifier,
@@ -46,81 +97,310 @@ fun DuplicateMangaDialog(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding( .padding(vertical = TabbedDialogPaddings.Vertical)
vertical = TabbedDialogPaddings.Vertical, .verticalScroll(rememberScrollState())
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(), .fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) { ) {
Text( Text(
modifier = Modifier.padding(TitlePadding), text = stringResource(MR.strings.possible_duplicates_title),
text = stringResource(MR.strings.are_you_sure),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(top = MaterialTheme.padding.small),
) )
Text( Text(
text = stringResource(MR.strings.confirm_add_duplicate_manga), text = stringResource(MR.strings.possible_duplicates_summary),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.then(horizontalPaddingModifier),
) )
Spacer(Modifier.height(PaddingSize)) LazyRow(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
TextPreferenceWidget( modifier = Modifier.height(getMaximumMangaCardHeight(duplicates)),
title = stringResource(MR.strings.action_show_manga), contentPadding = horizontalPadding,
icon = Icons.Outlined.Book,
onPreferenceClick = {
onDismissRequest()
onOpenManga()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_migrate_duplicate),
icon = Icons.Outlined.SwapVert,
onPreferenceClick = {
onDismissRequest()
onMigrate()
},
)
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
)
Row(
modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) { ) {
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) { items(
Text( items = duplicates,
modifier = Modifier key = { it.manga.id },
.padding(vertical = 8.dp), ) {
text = stringResource(MR.strings.action_cancel), DuplicateMangaListItem(
color = MaterialTheme.colorScheme.primary, duplicate = it,
style = MaterialTheme.typography.titleLarge, getSource = { sourceManager.getOrStub(it.manga.source) },
fontSize = 16.sp, onMigrate = { onMigrate(it.manga) },
onDismissRequest = onDismissRequest,
onOpenManga = { onOpenManga(it.manga) },
) )
} }
} }
Column(modifier = horizontalPaddingModifier) {
HorizontalDivider()
TextPreferenceWidget(
title = stringResource(MR.strings.action_add_anyway),
icon = Icons.Outlined.Add,
onPreferenceClick = {
onDismissRequest()
onConfirm()
},
modifier = Modifier.clip(CircleShape),
)
}
OutlinedButton(
onClick = onDismissRequest,
modifier = Modifier
.then(horizontalPaddingModifier)
.padding(bottom = MaterialTheme.padding.medium)
.heightIn(min = minHeight)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(vertical = MaterialTheme.padding.extraSmall),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge,
)
}
} }
} }
} }
private val PaddingSize = 16.dp @Composable
private fun DuplicateMangaListItem(
duplicate: MangaWithChapterCount,
getSource: () -> Source,
onDismissRequest: () -> Unit,
onOpenManga: () -> Unit,
onMigrate: () -> Unit,
) {
val source = getSource()
val manga = duplicate.manga
Column(
modifier = Modifier
.width(MangaCardWidth)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surface)
.combinedClickable(
onLongClick = { onOpenManga() },
onClick = {
onDismissRequest()
onMigrate()
},
)
.padding(MaterialTheme.padding.small),
) {
Box {
MangaCover.Book(
data = ImageRequest.Builder(LocalContext.current)
.data(manga)
.crossfade(true)
.build(),
modifier = Modifier.fillMaxWidth(),
)
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
) {
Badge(
color = MaterialTheme.colorScheme.secondary,
textColor = MaterialTheme.colorScheme.onSecondary,
text = pluralStringResource(
MR.plurals.manga_num_chapters,
duplicate.chapterCount.toInt(),
duplicate.chapterCount,
),
)
}
}
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp) Spacer(modifier = Modifier.height(MaterialTheme.padding.extraSmall))
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
Text(
text = manga.title,
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
if (!manga.author.isNullOrBlank()) {
MangaDetailRow(
text = manga.author!!,
iconImageVector = Icons.Filled.PersonOutline,
maxLines = 2,
)
}
if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
MangaDetailRow(
text = manga.artist!!,
iconImageVector = Icons.Filled.Brush,
maxLines = 2,
)
}
MangaDetailRow(
text = when (manga.status) {
SManga.ONGOING.toLong() -> stringResource(MR.strings.ongoing)
SManga.COMPLETED.toLong() -> stringResource(MR.strings.completed)
SManga.LICENSED.toLong() -> stringResource(MR.strings.licensed)
SManga.PUBLISHING_FINISHED.toLong() -> stringResource(MR.strings.publishing_finished)
SManga.CANCELLED.toLong() -> stringResource(MR.strings.cancelled)
SManga.ON_HIATUS.toLong() -> stringResource(MR.strings.on_hiatus)
else -> stringResource(MR.strings.unknown)
},
iconImageVector = when (manga.status) {
SManga.ONGOING.toLong() -> Icons.Outlined.Schedule
SManga.COMPLETED.toLong() -> Icons.Outlined.DoneAll
SManga.LICENSED.toLong() -> Icons.Outlined.AttachMoney
SManga.PUBLISHING_FINISHED.toLong() -> Icons.Outlined.Done
SManga.CANCELLED.toLong() -> Icons.Outlined.Close
SManga.ON_HIATUS.toLong() -> Icons.Outlined.Pause
else -> Icons.Outlined.Block
},
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
if (source is StubSource) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.error,
)
}
Text(
text = source.name,
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
@Composable
private fun MangaDetailRow(
text: String,
iconImageVector: ImageVector,
maxLines: Int = 1,
) {
Row(
modifier = Modifier
.secondaryItemAlpha()
.padding(top = MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = iconImageVector,
contentDescription = null,
modifier = Modifier.size(MangaDetailsIconWidth),
)
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
}
}
@Composable
private fun getMaximumMangaCardHeight(duplicates: List<MangaWithChapterCount>): Dp {
val density = LocalDensity.current
val typography = MaterialTheme.typography
val textMeasurer = rememberTextMeasurer()
val smallPadding = with(density) { MaterialTheme.padding.small.roundToPx() }
val extraSmallPadding = with(density) { MaterialTheme.padding.extraSmall.roundToPx() }
val width = with(density) { MangaCardWidth.roundToPx() - (2 * smallPadding) }
val iconWidth = with(density) { MangaDetailsIconWidth.roundToPx() }
val coverHeight = width / MangaCover.Book.ratio
val constraints = Constraints(maxWidth = width)
val detailsConstraints = Constraints(maxWidth = width - iconWidth - extraSmallPadding)
return remember(
duplicates,
density,
typography,
textMeasurer,
smallPadding,
extraSmallPadding,
coverHeight,
constraints,
detailsConstraints,
) {
duplicates.fastMaxOfOrNull {
calculateMangaCardHeight(
manga = it.manga,
density = density,
typography = typography,
textMeasurer = textMeasurer,
smallPadding = smallPadding,
extraSmallPadding = extraSmallPadding,
coverHeight = coverHeight,
constraints = constraints,
detailsConstraints = detailsConstraints,
)
}
?: 0.dp
}
}
private fun calculateMangaCardHeight(
manga: Manga,
density: Density,
typography: Typography,
textMeasurer: TextMeasurer,
smallPadding: Int,
extraSmallPadding: Int,
coverHeight: Float,
constraints: Constraints,
detailsConstraints: Constraints,
): Dp {
val titleHeight = textMeasurer.measureHeight(manga.title, typography.titleSmall, 2, constraints)
val authorHeight = if (!manga.author.isNullOrBlank()) {
textMeasurer.measureHeight(manga.author!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val artistHeight = if (!manga.artist.isNullOrBlank() && manga.author != manga.artist) {
textMeasurer.measureHeight(manga.artist!!, typography.bodySmall, 2, detailsConstraints)
} else {
0
}
val statusHeight = textMeasurer.measureHeight("", typography.bodySmall, 2, detailsConstraints)
val sourceHeight = textMeasurer.measureHeight("", typography.labelSmall, 1, constraints)
val totalHeight = coverHeight + titleHeight + authorHeight + artistHeight + statusHeight + sourceHeight
return with(density) { ((2 * smallPadding) + totalHeight + (5 * extraSmallPadding)).toDp() }
}
private fun TextMeasurer.measureHeight(
text: String,
style: TextStyle,
maxLines: Int,
constraints: Constraints,
): Int = measure(
text = text,
style = style,
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
constraints = constraints,
)
.size
.height
private val MangaCardWidth = 150.dp
private val MangaDetailsIconWidth = 16.dp
@@ -0,0 +1,45 @@
package eu.kanade.presentation.manga
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.manga.components.MangaNotesTextArea
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesScreen(
state: MangaNotesScreen.State,
navigateUp: () -> Unit,
onUpdate: (String) -> Unit,
) {
Scaffold(
topBar = { topBarScrollBehavior ->
AppBar(
titleContent = {
AppBarTitle(
title = stringResource(MR.strings.action_edit_notes),
subtitle = state.manga.title,
)
},
navigateUp = navigateUp,
scrollBehavior = topBarScrollBehavior,
)
},
) { contentPadding ->
MangaNotesTextArea(
state = state,
onUpdate = onUpdate,
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding)
.imePadding(),
)
}
}
@@ -142,6 +142,7 @@ fun MangaScreen(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditFetchIntervalClicked: (() -> Unit)?, onEditFetchIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY --> // SY -->
onMetadataViewerClicked: () -> Unit, onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit, onEditInfoClicked: () -> Unit,
@@ -201,6 +202,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked, onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY --> // SY -->
onMetadataViewerClicked = onMetadataViewerClicked, onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked, onEditInfoClicked = onEditInfoClicked,
@@ -247,6 +249,7 @@ fun MangaScreen(
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditFetchIntervalClicked, onEditIntervalClicked = onEditFetchIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onEditNotesClicked = onEditNotesClicked,
// SY --> // SY -->
onMetadataViewerClicked = onMetadataViewerClicked, onMetadataViewerClicked = onMetadataViewerClicked,
onEditInfoClicked = onEditInfoClicked, onEditInfoClicked = onEditInfoClicked,
@@ -303,6 +306,7 @@ private fun MangaScreenSmallImpl(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY --> // SY -->
onMetadataViewerClicked: () -> Unit, onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit, onEditInfoClicked: () -> Unit,
@@ -345,13 +349,9 @@ private fun MangaScreenSmallImpl(
} }
// SY <-- // SY <--
BackHandler(onBack = { BackHandler(enabled = isAnySelected) {
if (isAnySelected) { onAllChapterSelected(false)
onAllChapterSelected(false) }
} else {
navigateUp()
}
})
Scaffold( Scaffold(
topBar = { topBar = {
@@ -382,6 +382,7 @@ private fun MangaScreenSmallImpl(
onClickEditCategory = onEditCategoryClicked, onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY --> // SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow }, onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
@@ -519,8 +520,10 @@ private fun MangaScreenSmallImpl(
defaultExpandState = state.isFromSource, defaultExpandState = state.isFromSource,
description = state.manga.description, description = state.manga.description,
tagsProvider = { state.manga.genre }, tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch, onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard, onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY --> // SY -->
doSearch = onSearch, doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
@@ -626,6 +629,7 @@ fun MangaScreenLargeImpl(
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?, onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onEditNotesClicked: () -> Unit,
// SY --> // SY -->
onMetadataViewerClicked: () -> Unit, onMetadataViewerClicked: () -> Unit,
onEditInfoClicked: () -> Unit, onEditInfoClicked: () -> Unit,
@@ -672,13 +676,9 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState() val chapterListState = rememberLazyListState()
BackHandler(onBack = { BackHandler(enabled = isAnySelected) {
if (isAnySelected) { onAllChapterSelected(false)
onAllChapterSelected(false) }
} else {
navigateUp()
}
})
Scaffold( Scaffold(
topBar = { topBar = {
@@ -696,6 +696,7 @@ fun MangaScreenLargeImpl(
onClickEditCategory = onEditCategoryClicked, onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh, onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked, onClickMigrate = onMigrateClicked,
onClickEditNotes = onEditNotesClicked,
// SY --> // SY -->
onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite }, onClickEditInfo = onEditInfoClicked.takeIf { state.manga.favorite },
onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow }, onClickRecommend = onRecommendClicked.takeIf { state.showRecommendationsInOverflow },
@@ -814,8 +815,10 @@ fun MangaScreenLargeImpl(
defaultExpandState = true, defaultExpandState = true,
description = state.manga.description, description = state.manga.description,
tagsProvider = { state.manga.genre }, tagsProvider = { state.manga.genre },
notes = state.manga.notes,
onTagSearch = onTagSearch, onTagSearch = onTagSearch,
onCopyTagToClipboard = onCopyTagToClipboard, onCopyTagToClipboard = onCopyTagToClipboard,
onEditNotes = onEditNotesClicked,
// SY --> // SY -->
doSearch = onSearch, doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) { searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
@@ -3,6 +3,9 @@ package eu.kanade.presentation.manga.components
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Build import android.os.Build
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -25,18 +28,22 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil3.asDrawable import coil3.asDrawable
import coil3.imageLoader import coil3.imageLoader
@@ -49,11 +56,14 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.manga.EditCoverAction import eu.kanade.presentation.manga.EditCoverAction
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import soup.compose.material.motion.MotionConstants
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.PredictiveBack
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import kotlin.coroutines.cancellation.CancellationException
@Composable @Composable
fun MangaCoverDialog( fun MangaCoverDialog(
@@ -152,10 +162,32 @@ fun MangaCoverDialog(
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() } val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() } val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
var scale by remember { mutableFloatStateOf(1f) }
PredictiveBackHandler { progress ->
try {
progress.collect { backEvent ->
scale = lerp(1f, 0.8f, PredictiveBack.transform(backEvent.progress))
}
onDismissRequest()
} catch (e: CancellationException) {
animate(
initialValue = scale,
targetValue = 1f,
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
) { value, _ ->
scale = value
}
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clickableNoIndication(onClick = onDismissRequest), .clickableNoIndication(onClick = onDismissRequest)
.graphicsLayer {
scaleX = scale
scaleY = scale
},
) { ) {
AndroidView( AndroidView(
factory = { factory = {
@@ -172,20 +204,20 @@ fun MangaCoverDialog(
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.target { image -> .target { image ->
val drawable = image.asDrawable(view.context.resources) val drawable = image.asDrawable(view.context.resources)
// Copy bitmap in case it came from memory cache // Copy bitmap in case it came from memory cache
// Because SSIV needs to thoroughly read the image // Because SSIV needs to thoroughly read the image
val copy = (drawable as? BitmapDrawable)?.let { val copy = (drawable as? BitmapDrawable)
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ?.bitmap
Bitmap.Config.HARDWARE ?.copy(
} else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Bitmap.Config.ARGB_8888 Bitmap.Config.HARDWARE
} } else {
BitmapDrawable( Bitmap.Config.ARGB_8888
view.context.resources, },
it.bitmap.copy(config, false), false,
) )
} ?: drawable ?.toDrawable(view.context.resources)
?: drawable
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
} }
.build() .build()
@@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotatorConfig
import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -95,8 +97,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable @Composable
fun MangaInfoBox( fun MangaInfoBox(
isTabletUi: Boolean, isTabletUi: Boolean,
@@ -250,8 +250,10 @@ fun ExpandableMangaDescription(
defaultExpandState: Boolean, defaultExpandState: Boolean,
description: String?, description: String?,
tagsProvider: () -> List<String>?, tagsProvider: () -> List<String>?,
notes: String,
onTagSearch: (String) -> Unit, onTagSearch: (String) -> Unit,
onCopyTagToClipboard: (tag: String) -> Unit, onCopyTagToClipboard: (tag: String) -> Unit,
onEditNotes: () -> Unit,
// SY --> // SY -->
searchMetadataChips: SearchMetadataChips?, searchMetadataChips: SearchMetadataChips?,
doSearch: (query: String, global: Boolean) -> Unit, doSearch: (query: String, global: Boolean) -> Unit,
@@ -264,15 +266,12 @@ fun ExpandableMangaDescription(
} }
val desc = val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder) description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary( MangaSummary(
expandedDescription = desc, description = desc,
shrunkDescription = trimmedDescription,
expanded = expanded, expanded = expanded,
notes = notes,
onEditNotesClicked = onEditNotes,
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@@ -594,11 +593,26 @@ private fun ColumnScope.MangaContentInfo(
} }
} }
private val descriptionAnnotator = markdownAnnotator(
annotate = { content, child ->
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
append(content.substring(child.startOffset, child.endOffset))
return@markdownAnnotator true
}
false
},
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
@Composable @Composable
private fun MangaSummary( private fun MangaSummary(
expandedDescription: String, description: String,
shrunkDescription: String, notes: String,
expanded: Boolean, expanded: Boolean,
onEditNotesClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val animProgress by animateFloatAsState( val animProgress by animateFloatAsState(
@@ -610,25 +624,40 @@ private fun MangaSummary(
contents = listOf( contents = listOf(
{ {
Text( Text(
text = "\n\n", // Shows at least 3 lines // Shows at least 3 lines if no notes
// when there are notes show 6
text = if (notes.isBlank()) "\n\n" else "\n\n\n\n\n",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
}, },
{ {
Text( Column {
text = expandedDescription, MangaNotesSection(
style = MaterialTheme.typography.bodyMedium, content = notes,
) expanded = true,
}, onEditNotes = onEditNotesClicked,
{
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.secondaryItemAlpha(),
) )
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
},
{
Column {
MangaNotesSection(
content = notes,
expanded = expanded,
onEditNotes = onEditNotesClicked,
)
SelectionContainer {
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator,
)
}
} }
}, },
{ {
@@ -0,0 +1,60 @@
package eu.kanade.presentation.manga.components
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichText
private val FADE_TIME = tween<Float>(500)
@Composable
fun MangaNotesDisplay(
content: String,
modifier: Modifier,
) {
val alpha = remember { Animatable(1f) }
var contentUpdatedOnce by remember { mutableStateOf(false) }
val richTextState = rememberRichTextState()
val primaryColor = MaterialTheme.colorScheme.primary
LaunchedEffect(content) {
richTextState.setMarkdown(content)
if (!contentUpdatedOnce) {
contentUpdatedOnce = true
return@LaunchedEffect
}
alpha.snapTo(targetValue = 0f)
alpha.animateTo(targetValue = 1f, animationSpec = FADE_TIME)
}
LaunchedEffect(Unit) {
richTextState.config.unorderedListIndent = 4
richTextState.config.orderedListIndent = 20
}
LaunchedEffect(primaryColor) {
richTextState.config.linkColor = primaryColor
}
SelectionContainer {
RichText(
modifier = modifier
// Only animate size if the notes changes
.then(if (contentUpdatedOnce) Modifier.animateContentSize() else Modifier)
.alpha(alpha.value),
style = MaterialTheme.typography.bodyMedium,
state = richTextState,
)
}
}
@@ -0,0 +1,90 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.ButtonDefaults
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun MangaNotesSection(
content: String,
expanded: Boolean,
onEditNotes: () -> Unit,
modifier: Modifier = Modifier,
) {
if (content.isBlank()) return
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
MangaNotesDisplay(
content = content,
modifier = modifier.fillMaxWidth(),
)
if (expanded) {
Button(
onClick = onEditNotes,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.EditNote,
contentDescription = null,
modifier = Modifier
.size(16.dp),
)
Text(
stringResource(MR.strings.action_edit_notes),
)
}
}
}
HorizontalDivider(
modifier = Modifier
.padding(
top = if (expanded) 0.dp else 12.dp,
bottom = if (expanded) 16.dp else 12.dp,
),
)
}
}
@PreviewLightDark
@Composable
private fun MangaNotesSectionPreview() {
MangaNotesSection(
onEditNotes = {},
expanded = true,
content = "# Hello world\ntest1234 hi there!",
)
}
@@ -0,0 +1,224 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.outlined.FormatBold
import androidx.compose.material.icons.outlined.FormatItalic
import androidx.compose.material.icons.outlined.FormatListNumbered
import androidx.compose.material.icons.outlined.FormatUnderlined
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditorDefaults.richTextEditorColors
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds
private const val MAX_LENGTH = 250
private const val MAX_LENGTH_WARN = MAX_LENGTH * 0.9
@Composable
fun MangaNotesTextArea(
state: MangaNotesScreen.State,
onUpdate: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
val richTextState = rememberRichTextState()
val primaryColor = MaterialTheme.colorScheme.primary
DisposableEffect(scope, richTextState) {
snapshotFlow { richTextState.annotatedString }
.debounce(0.25.seconds)
.distinctUntilChanged()
.map { richTextState.toMarkdown() }
.onEach { onUpdate(it) }
.launchIn(scope)
onDispose {
onUpdate(richTextState.toMarkdown())
}
}
LaunchedEffect(Unit) {
richTextState.setMarkdown(state.notes)
richTextState.config.unorderedListIndent = 4
richTextState.config.orderedListIndent = 20
}
LaunchedEffect(primaryColor) {
richTextState.config.linkColor = primaryColor
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(focusRequester) {
focusRequester.requestFocus()
}
val textLength = remember(richTextState.annotatedString) { richTextState.toText().length }
Column(
modifier = modifier
.padding(horizontal = MaterialTheme.padding.small)
.fillMaxSize(),
) {
RichTextEditor(
state = richTextState,
textStyle = MaterialTheme.typography.bodyLarge,
maxLength = MAX_LENGTH,
placeholder = {
Text(text = stringResource(MR.strings.notes_placeholder))
},
colors = richTextEditorColors(
containerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
contentPadding = PaddingValues(
horizontal = MaterialTheme.padding.medium,
),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.focusRequester(focusRequester),
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(vertical = MaterialTheme.padding.small)
.fillMaxWidth(),
) {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold)) },
isSelected = richTextState.currentSpanStyle.fontWeight == FontWeight.Bold,
icon = Icons.Outlined.FormatBold,
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic)) },
isSelected = richTextState.currentSpanStyle.fontStyle == FontStyle.Italic,
icon = Icons.Outlined.FormatItalic,
)
}
item {
MangaNotesTextAreaButton(
onClick = {
richTextState.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
},
isSelected = richTextState.currentSpanStyle.textDecoration
?.contains(TextDecoration.Underline)
?: false,
icon = Icons.Outlined.FormatUnderlined,
)
}
item {
VerticalDivider(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.extraSmall)
.height(MaterialTheme.padding.large),
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleUnorderedList() },
isSelected = richTextState.isUnorderedList,
icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
)
}
item {
MangaNotesTextAreaButton(
onClick = { richTextState.toggleOrderedList() },
isSelected = richTextState.isOrderedList,
icon = Icons.Outlined.FormatListNumbered,
)
}
}
Box(
contentAlignment = Alignment.Center,
) {
Text(
text = (MAX_LENGTH - textLength).toString(),
color = if (textLength > MAX_LENGTH_WARN) {
MaterialTheme.colorScheme.error
} else {
Color.Unspecified
},
modifier = Modifier.padding(MaterialTheme.padding.extraSmall),
)
}
}
}
}
@Composable
fun MangaNotesTextAreaButton(
onClick: () -> Unit,
icon: ImageVector,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.clip(MaterialTheme.shapes.small)
.clickable(
onClick = onClick,
enabled = true,
role = Role.Button,
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = icon,
contentDescription = icon.name,
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary,
modifier = Modifier
.background(color = if (isSelected) MaterialTheme.colorScheme.onBackground else Color.Transparent)
.padding(MaterialTheme.padding.extraSmall),
)
}
}
@@ -38,6 +38,7 @@ fun MangaToolbar(
onClickEditCategory: (() -> Unit)?, onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?, onClickMigrate: (() -> Unit)?,
onClickEditNotes: () -> Unit,
// SY --> // SY -->
onClickEditInfo: (() -> Unit)?, onClickEditInfo: (() -> Unit)?,
onClickRecommend: (() -> Unit)?, onClickRecommend: (() -> Unit)?,
@@ -147,6 +148,12 @@ fun MangaToolbar(
), ),
) )
} }
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_notes),
onClick = onClickEditNotes,
),
)
// SY --> // SY -->
if (onClickMerge != null) { if (onClickMerge != null) {
add( add(
@@ -0,0 +1,253 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.compose.Markdown
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
import com.mikepenz.markdown.compose.elements.MarkdownDivider
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
import com.mikepenz.markdown.compose.elements.MarkdownTable
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
import com.mikepenz.markdown.compose.elements.MarkdownText
import com.mikepenz.markdown.compose.elements.listDepth
import com.mikepenz.markdown.model.DefaultMarkdownColors
import com.mikepenz.markdown.model.DefaultMarkdownTypography
import com.mikepenz.markdown.model.MarkdownAnnotator
import com.mikepenz.markdown.model.MarkdownColors
import com.mikepenz.markdown.model.MarkdownPadding
import com.mikepenz.markdown.model.MarkdownTypography
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.rememberMarkdownState
import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.flavours.commonmark.CommonMarkMarkerProcessor
import org.intellij.markdown.flavours.gfm.table.GitHubTableMarkerProvider
import org.intellij.markdown.parser.MarkerProcessor
import org.intellij.markdown.parser.MarkerProcessorFactory
import org.intellij.markdown.parser.ProductionHolder
import org.intellij.markdown.parser.constraints.CommonMarkdownConstraints
import org.intellij.markdown.parser.constraints.MarkdownConstraints
import org.intellij.markdown.parser.markerblocks.MarkerBlockProvider
import org.intellij.markdown.parser.markerblocks.providers.AtxHeaderProvider
import org.intellij.markdown.parser.markerblocks.providers.BlockQuoteProvider
import org.intellij.markdown.parser.markerblocks.providers.CodeBlockProvider
import org.intellij.markdown.parser.markerblocks.providers.CodeFenceProvider
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider
import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider
import tachiyomi.presentation.core.components.material.padding
@Composable
fun MarkdownRender(
content: String,
modifier: Modifier = Modifier,
flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor,
annotator: MarkdownAnnotator = remember { markdownAnnotator() },
) {
Markdown(
markdownState = rememberMarkdownState(
content = content,
flavour = flavour,
immediate = true,
),
annotator = annotator,
colors = getMarkdownColors(),
typography = getMarkdownTypography(),
padding = markdownPadding,
components = markdownComponents,
imageTransformer = Coil3ImageTransformerImpl,
modifier = modifier,
)
}
@Composable
@ReadOnlyComposable
private fun getMarkdownColors(): MarkdownColors {
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
return DefaultMarkdownColors(
text = MaterialTheme.colorScheme.onSurface,
codeText = Color.Unspecified,
inlineCodeText = Color.Unspecified,
linkText = Color.Unspecified,
codeBackground = codeBackground,
inlineCodeBackground = codeBackground,
dividerColor = MaterialTheme.colorScheme.outlineVariant,
tableText = Color.Unspecified,
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
)
}
@Composable
@ReadOnlyComposable
private fun getMarkdownTypography(): MarkdownTypography {
val link = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
)
return DefaultMarkdownTypography(
h1 = MaterialTheme.typography.headlineMedium,
h2 = MaterialTheme.typography.headlineSmall,
h3 = MaterialTheme.typography.titleLarge,
h4 = MaterialTheme.typography.titleMedium,
h5 = MaterialTheme.typography.titleSmall,
h6 = MaterialTheme.typography.bodyLarge,
text = MaterialTheme.typography.bodyMedium,
code = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
inlineCode = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
quote = MaterialTheme.typography.bodyMedium.plus(SpanStyle(fontStyle = FontStyle.Italic)),
paragraph = MaterialTheme.typography.bodyMedium,
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = link,
textLink = TextLinkStyles(style = link.toSpanStyle()),
table = MaterialTheme.typography.bodyMedium,
)
}
private val markdownPadding = object : MarkdownPadding {
override val block: Dp = 2.dp
override val blockQuote: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 0.dp)
override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(
left = 4.dp,
top = 2.dp,
right = 4.dp,
bottom = 2.dp,
)
override val blockQuoteText: PaddingValues = PaddingValues(vertical = 4.dp)
override val codeBlock: PaddingValues = PaddingValues(8.dp)
override val list: Dp = 0.dp
override val listIndent: Dp = 8.dp
override val listItemBottom: Dp = 0.dp
override val listItemTop: Dp = 0.dp
}
private val markdownComponents = markdownComponents(
horizontalRule = {
MarkdownDivider(
modifier = Modifier
.padding(vertical = MaterialTheme.padding.extraSmall)
.fillMaxWidth(),
)
},
orderedList = { ol ->
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownOrderedList(
content = ol.content,
node = ol.node,
style = ol.typography.ordered,
depth = ol.listDepth,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
},
unorderedList = { ul ->
val markers = listOf("", "", "", "")
CompositionLocalProvider(
LocalBulletListHandler provides { _, _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
) {
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownBulletList(
content = ul.content,
node = ul.node,
style = ul.typography.bullet,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
}
},
table = { t ->
MarkdownTable(
content = t.content,
node = t.node,
style = t.typography.text,
headerBlock = { content, header, tableWidth, style ->
MarkdownTableHeader(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
rowBlock = { content, header, tableWidth, style ->
MarkdownTableRow(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
)
},
custom = { type, model ->
if (type in DISALLOWED_MARKDOWN_TYPES) {
MarkdownText(
content = model.content.substring(model.node.startOffset, model.node.endOffset),
style = model.typography.text,
)
}
},
)
private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() {
override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory
}
private object SimpleMarkdownProcessFactory : MarkerProcessorFactory {
override fun createMarkerProcessor(productionHolder: ProductionHolder): MarkerProcessor<*> {
return SimpleMarkdownMarkerProcessor(productionHolder, CommonMarkdownConstraints.BASE)
}
}
/**
* Like `CommonMarkFlavour`, but with html blocks and reference links removed and
* table support added
*/
private class SimpleMarkdownMarkerProcessor(
productionHolder: ProductionHolder,
constraints: MarkdownConstraints,
) : CommonMarkMarkerProcessor(productionHolder, constraints) {
private val markerBlockProviders = listOf(
CodeBlockProvider(),
HorizontalRuleProvider(),
CodeFenceProvider(),
SetextHeaderProvider(),
BlockQuoteProvider(),
ListMarkerProvider(),
AtxHeaderProvider(),
GitHubTableMarkerProvider(),
)
override fun getMarkerBlockProviders(): List<MarkerBlockProvider<StateInfo>> {
return markerBlockProviders
}
}
val DISALLOWED_MARKDOWN_TYPES = arrayOf(HTML_TAG)
@@ -1,5 +1,6 @@
package eu.kanade.presentation.more package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -13,13 +14,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown import eu.kanade.presentation.manga.components.MarkdownRender
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -42,17 +40,15 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now), rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate, onRejectClick = onRejectUpdate,
) { ) {
RichText( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large), .padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) { ) {
Markdown(content = changelogInfo) MarkdownRender(
content = changelogInfo,
flavour = GFMFlavourDescriptor(),
)
TextButton( TextButton(
onClick = onOpenInBrowser, onClick = onOpenInBrowser,
@@ -42,7 +42,9 @@ fun OnboardingScreen(
} }
val isLastStep = currentStep == steps.lastIndex val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) BackHandler(enabled = currentStep != 0) {
currentStep--
}
InfoScreen( InfoScreen(
icon = Icons.Outlined.RocketLaunch, icon = Icons.Outlined.RocketLaunch,
@@ -88,6 +88,7 @@ import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetAllManga import tachiyomi.domain.manga.interactor.GetAllManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -114,6 +115,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() } val basePreferences = remember { Injekt.get<BasePreferences>() }
val networkPreferences = remember { Injekt.get<NetworkPreferences>() } val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
return listOf( return listOf(
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -154,7 +156,7 @@ object SettingsAdvancedScreen : SearchableSettings {
getBackgroundActivityGroup(), getBackgroundActivityGroup(),
getDataGroup(), getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences), getNetworkGroup(networkPreferences = networkPreferences),
getLibraryGroup(), getLibraryGroup(libraryPreferences = libraryPreferences),
getReaderGroup(basePreferences = basePreferences), getReaderGroup(basePreferences = basePreferences),
getExtensionsGroup(basePreferences = basePreferences), getExtensionsGroup(basePreferences = basePreferences),
// SY --> // SY -->
@@ -322,7 +324,9 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
@Composable @Composable
private fun getLibraryGroup(): Preference.PreferenceGroup { private fun getLibraryGroup(
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
@@ -350,6 +354,11 @@ object SettingsAdvancedScreen : SearchableSettings {
} }
}, },
), ),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.updateMangaTitles(),
title = stringResource(MR.strings.pref_update_library_manga_titles),
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
),
), ),
) )
} }
@@ -30,8 +30,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -228,7 +231,9 @@ object SettingsTrackingScreen : SearchableSettings {
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Username + ContentType.EmailAddress },
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
label = { Text(text = stringResource(uNameStringRes)) }, label = { Text(text = stringResource(uNameStringRes)) },
@@ -239,7 +244,9 @@ object SettingsTrackingScreen : SearchableSettings {
var hidePassword by remember { mutableStateOf(true) } var hidePassword by remember { mutableStateOf(true) }
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.semantics { contentType = ContentType.Password },
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
label = { Text(text = stringResource(MR.strings.password)) }, label = { Text(text = stringResource(MR.strings.password)) },
@@ -288,7 +295,7 @@ object SettingsTrackingScreen : SearchableSettings {
} }
}, },
) { ) {
val id = if (processing) MR.strings.loading else MR.strings.login val id = if (processing) MR.strings.logging_in else MR.strings.login
Text(text = stringResource(id)) Text(text = stringResource(id))
} }
}, },
@@ -6,7 +6,7 @@ import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -1,8 +1,10 @@
package eu.kanade.presentation.more.settings.screen.advanced package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -12,6 +14,7 @@ import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -42,16 +45,16 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchUI import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.toLong
import tachiyomi.core.common.util.lang.withNonCancellableContext import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.model.SourceWithCount import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@@ -73,18 +76,45 @@ class ClearDatabaseScreen : Screen() {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen() is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> { is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) { if (s.showConfirmation) {
// SY -->
var keepReadManga by remember { mutableStateOf(true) } var keepReadManga by remember { mutableStateOf(true) }
// SY <--
AlertDialog( AlertDialog(
title = {
Text(text = stringResource(MR.strings.are_you_sure))
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.clear_database_text))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(MR.strings.clear_db_exclude_read),
modifier = Modifier.weight(1f),
)
Switch(
checked = keepReadManga,
onCheckedChange = { keepReadManga = it },
)
}
if (!keepReadManga) {
Text(
text = stringResource(MR.strings.clear_database_history_warning),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
)
}
}
},
onDismissRequest = model::hideConfirmation, onDismissRequest = model::hideConfirmation,
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
scope.launchUI { scope.launchUI {
// SY -->
model.removeMangaBySourceId(keepReadManga) model.removeMangaBySourceId(keepReadManga)
// SY <--
model.clearSelection() model.clearSelection()
model.hideConfirmation() model.hideConfirmation()
context.toast(MR.strings.clear_database_completed) context.toast(MR.strings.clear_database_completed)
@@ -99,20 +129,6 @@ class ClearDatabaseScreen : Screen() {
Text(text = stringResource(MR.strings.action_cancel)) Text(text = stringResource(MR.strings.action_cancel))
} }
}, },
text = {
// SY -->
Column {
// SY <--
Text(text = stringResource(MR.strings.clear_database_confirmation))
// SY -->
LabeledCheckbox(
label = stringResource(SYMR.strings.clear_db_exclude_read),
checked = keepReadManga,
onCheckedChange = { keepReadManga = it },
)
}
// SY <--
},
) )
} }
@@ -224,15 +240,9 @@ private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenMod
} }
} }
suspend fun removeMangaBySourceId(/* SY --> */keepReadManga: Boolean /* SY <-- */) = withNonCancellableContext { suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext {
val state = state.value as? State.Ready ?: return@withNonCancellableContext val state = state.value as? State.Ready ?: return@withNonCancellableContext
// SY --> database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong())
if (keepReadManga) {
database.mangasQueries.deleteMangasNotInLibraryAndNotReadBySourceIds(state.selection)
} else {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
}
// SY <--
database.historyQueries.removeResettedHistory() database.historyQueries.removeResettedHistory()
} }
@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -86,7 +85,8 @@ internal fun BasePreferenceWidget(
} }
} }
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed { @Composable
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier {
var highlightFlag by remember { mutableStateOf(false) } var highlightFlag by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (highlighted) { if (highlighted) {
@@ -116,7 +116,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
}, },
label = "highlight", label = "highlight",
) )
Modifier.background(color = highlight) return this.background(color = highlight)
} }
internal val TrailingWidgetBuffer = 16.dp internal val TrailingWidgetBuffer = 16.dp
@@ -60,7 +60,9 @@ fun UpdateScreen(
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit, onOpenChapter: (UpdatesItem) -> Unit,
) { ) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) BackHandler(enabled = state.selectionMode) {
onSelectAll(false)
}
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
@@ -1,12 +1,46 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.annotation.SuppressLint
import androidx.activity.BackEventCompat
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SeekableTransitionState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.ScreenModelStore import cafe.adriel.voyager.core.model.ScreenModelStore
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
@@ -15,18 +49,28 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransitionContent import cafe.adriel.voyager.transitions.ScreenTransitionContent
import eu.kanade.tachiyomi.util.view.getWindowRadius
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import soup.compose.material.motion.animation.materialSharedAxisX import soup.compose.material.motion.animation.materialSharedAxisXIn
import soup.compose.material.motion.animation.materialSharedAxisXOut
import soup.compose.material.motion.animation.rememberSlideDistance import soup.compose.material.motion.animation.rememberSlideDistance
import tachiyomi.presentation.core.util.PredictiveBack
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.absoluteValue
/** /**
* For invoking back press to the parent activity * For invoking back press to the parent activity
*/ */
@SuppressLint("ComposeCompositionLocalUsage")
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
interface Tab : cafe.adriel.voyager.navigator.tab.Tab { interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
@@ -59,39 +103,278 @@ interface AssistContentScreen {
fun onProvideAssistUrl(): String? fun onProvideAssistUrl(): String?
} }
@OptIn(InternalVoyagerApi::class)
@Composable @Composable
fun DefaultNavigatorScreenTransition( fun DefaultNavigatorScreenTransition(
navigator: Navigator, navigator: Navigator,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val slideDistance = rememberSlideDistance() val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
mutableStateOf(emptySet())
}
val currentScreens = navigator.items
DisposableEffect(currentScreens) {
onDispose {
val newScreenKeys = navigator.items.map { it.key }
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
}
}
val slideDistance = rememberSlideDistance(slideDistance = 30.dp)
ScreenTransition( ScreenTransition(
navigator = navigator, navigator = navigator,
transition = {
materialSharedAxisX(
forward = navigator.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
modifier = modifier, modifier = modifier,
enterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
}
},
exitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
}
},
popEnterTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
}
},
popExitTransition = {
if (it == SwipeEdge.Right) {
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
} else {
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
}
},
content = { screen ->
if (this.transition.targetState == this.transition.currentState) {
LaunchedEffect(Unit) {
val newScreens = navigator.items.map { it.key }
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
if (screensToDispose.isNotEmpty()) {
screensToDispose.forEach { navigator.dispose(it) }
navigator.clearEvent()
}
screenCandidatesToDispose.value = emptySet()
}
}
screen.Content()
},
) )
} }
enum class SwipeEdge {
Unknown,
Left,
Right,
}
private enum class AnimationType {
Pop,
Cancel,
}
@Composable @Composable
fun ScreenTransition( fun ScreenTransition(
navigator: Navigator, navigator: Navigator,
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
content: ScreenTransitionContent = { it.Content() }, content: ScreenTransitionContent = { it.Content() },
) { ) {
AnimatedContent( val view = LocalView.current
targetState = navigator.lastItem, val viewConfig = LocalViewConfiguration.current
transitionSpec = transition, val scope = rememberCoroutineScope()
val state = remember {
ScreenTransitionState(
navigator = navigator,
scope = scope,
flingAnimationSpec = flingAnimationSpec(),
windowCornerRadius = view.getWindowRadius().toFloat(),
)
}
val transitionState = remember { SeekableTransitionState(navigator.lastItem) }
val transition = rememberTransition(transitionState = transitionState)
if (state.isPredictiveBack || state.isAnimating) {
LaunchedEffect(state.progress) {
if (!state.isPredictiveBack) return@LaunchedEffect
val previousEntry = navigator.items.getOrNull(navigator.size - 2)
if (previousEntry != null) {
transitionState.seekTo(fraction = state.progress, targetState = previousEntry)
}
}
} else {
LaunchedEffect(navigator) {
snapshotFlow { navigator.lastItem }
.collect {
state.cancelCancelAnimation()
if (it != transitionState.currentState) {
transitionState.animateTo(it)
} else {
transitionState.snapTo(it)
}
}
}
}
PredictiveBackHandler(enabled = navigator.canPop) { backEvent ->
state.cancelCancelAnimation()
var startOffset: Offset? = null
backEvent
.dropWhile {
if (startOffset == null) startOffset = Offset(it.touchX, it.touchY)
if (state.isAnimating) return@dropWhile true
// Touch slop check
val diff = Offset(it.touchX, it.touchY) - startOffset!!
diff.x.absoluteValue < viewConfig.touchSlop && diff.y.absoluteValue < viewConfig.touchSlop
}
.onCompletion {
if (it == null) {
state.finish()
} else {
state.cancel()
}
}
.collect {
state.setPredictiveBackProgress(
progress = it.progress,
swipeEdge = when (it.swipeEdge) {
BackEventCompat.EDGE_LEFT -> SwipeEdge.Left
BackEventCompat.EDGE_RIGHT -> SwipeEdge.Right
else -> SwipeEdge.Unknown
},
)
}
}
transition.AnimatedContent(
modifier = modifier, modifier = modifier,
label = "transition", transitionSpec = {
) { screen -> val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack
navigator.saveableState("transition", screen) { ContentTransform(
content(screen) targetContentEnter = if (pop) {
popEnterTransition(state.swipeEdge)
} else {
enterTransition(state.swipeEdge)
},
initialContentExit = if (pop) {
popExitTransition(state.swipeEdge)
} else {
exitTransition(state.swipeEdge)
},
targetContentZIndex = if (pop) 0f else 1f,
sizeTransform = sizeTransform?.invoke(this),
)
},
contentKey = { it.key },
) {
navigator.saveableState("transition", it) {
content(it)
} }
} }
} }
@Stable
private class ScreenTransitionState(
private val navigator: Navigator,
private val scope: CoroutineScope,
private val flingAnimationSpec: AnimationSpec<Float>,
windowCornerRadius: Float,
) {
var isPredictiveBack: Boolean by mutableStateOf(false)
private set
var progress: Float by mutableFloatStateOf(0f)
private set
var swipeEdge: SwipeEdge by mutableStateOf(SwipeEdge.Unknown)
private set
private var animationJob: Pair<Job, AnimationType>? by mutableStateOf(null)
val isAnimating: Boolean
get() = animationJob?.first?.isActive == true
val windowCornerShape = RoundedCornerShape(windowCornerRadius)
private fun reset() {
this.isPredictiveBack = false
this.swipeEdge = SwipeEdge.Unknown
this.animationJob = null
}
fun setPredictiveBackProgress(progress: Float, swipeEdge: SwipeEdge) {
this.progress = lerp(0f, 0.65f, PredictiveBack.transform(progress))
this.swipeEdge = swipeEdge
this.isPredictiveBack = true
}
fun finish() {
if (!isPredictiveBack) {
navigator.pop()
return
}
animationJob = scope.launch {
try {
animate(
initialValue = progress,
targetValue = 1f,
animationSpec = flingAnimationSpec,
block = { i, _ -> progress = i },
)
navigator.pop()
} catch (e: CancellationException) {
// Cancelled
progress = 0f
} finally {
reset()
}
} to AnimationType.Pop
}
fun cancel() {
if (!isPredictiveBack) {
return
}
animationJob = scope.launch {
try {
animate(
initialValue = progress,
targetValue = 0f,
animationSpec = flingAnimationSpec,
block = { i, _ -> progress = i },
)
} catch (e: CancellationException) {
// Cancelled
progress = 1f
} finally {
reset()
}
} to AnimationType.Cancel
}
fun cancelCancelAnimation() {
if (animationJob?.second == AnimationType.Cancel) {
animationJob?.first?.cancel()
animationJob = null
}
}
}
private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<Screen>>, List<Screen>> {
return Saver(
save = { it.value.toList() },
restore = { mutableStateOf(it.toSet()) },
)
}
@@ -3,7 +3,6 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -27,7 +26,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient import com.kevinnzou.web.AccompanistWebViewClient
@@ -39,19 +37,13 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable @Composable
fun WebViewScreenContent( fun WebViewScreenContent(
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
@@ -65,11 +57,8 @@ fun WebViewScreenContent(
) { ) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator() val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) } var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) } var showCloudflareHelp by remember { mutableStateOf(false) }
@@ -124,40 +113,6 @@ fun WebViewScreenContent(
} }
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
return try {
val internalRequest = Request.Builder().apply {
url(request!!.url.toString())
request.requestHeaders.forEach { (key, value) ->
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
return@forEach
}
addHeader(key, value)
}
method(request.method, null)
}.build()
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
WebResourceResponse(
contentType,
contentEncoding,
response.code,
response.message,
response.headers.associate { it.first to it.second },
response.body.byteStream(),
)
} catch (e: Throwable) {
super.shouldInterceptRequest(view, request)
}
}
} }
} }
@@ -80,7 +80,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.stringResource(MR.strings.action_share), context.stringResource(MR.strings.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri), NotificationReceiver.shareBackupPendingActivity(context, file.uri),
) )
show(Notifications.ID_BACKUP_COMPLETE) show(Notifications.ID_BACKUP_COMPLETE)
@@ -135,6 +135,7 @@ private fun Manga.toBackupManga(/* SY --> */customMangaInfo: CustomMangaInfo?/*
lastModifiedAt = this.lastModifiedAt, lastModifiedAt = this.lastModifiedAt,
favoriteModifiedAt = this.favoriteModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt,
version = this.version, version = this.version,
notes = this.notes,
// SY --> // SY -->
).also { backupManga -> ).also { backupManga ->
customMangaInfo?.let { customMangaInfo?.let {
@@ -38,8 +38,10 @@ data class BackupManga(
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE, @ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
@ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(106) var lastModifiedAt: Long = 0,
@ProtoNumber(107) var favoriteModifiedAt: Long? = null, @ProtoNumber(107) var favoriteModifiedAt: Long? = null,
// Mihon values start here
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(), @ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
@ProtoNumber(109) var version: Long = 0, @ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var notes: String = "",
// SY specific values // SY specific values
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(), @ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@@ -77,6 +79,7 @@ data class BackupManga(
lastModifiedAt = this@BackupManga.lastModifiedAt, lastModifiedAt = this@BackupManga.lastModifiedAt,
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
version = this@BackupManga.version, version = this@BackupManga.version,
notes = this@BackupManga.notes,
) )
} }
} }
@@ -159,6 +159,7 @@ class MangaRestorer(
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
version = manga.version, version = manga.version,
isSyncing = 1, isSyncing = 1,
notes = manga.notes,
) )
} }
return manga return manga
@@ -292,6 +293,7 @@ class MangaRestorer(
dateAdded = manga.dateAdded, dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy, updateStrategy = manga.updateStrategy,
version = manga.version, version = manga.version,
notes = manga.notes,
) )
mangasQueries.selectLastInsertedRowId() mangasQueries.selectLastInsertedRowId()
} }
@@ -307,6 +307,41 @@ class DownloadCache(
notifyChanges() notifyChanges()
} }
/**
* Renames a manga in this cache.
*
* @param manga the manga being renamed.
* @param mangaUniFile the manga's new directory.
* @param newTitle the manga's new title.
*/
suspend fun renameManga(manga: Manga, mangaUniFile: UniFile, newTitle: String) {
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val oldMangaDirName = provider.getMangaDirName(/* SY --> */ manga.ogTitle /* SY <-- */)
var oldChapterDirs: MutableSet<String>? = null
// Save the old name's cached chapter dirs
if (sourceDir.mangaDirs.containsKey(oldMangaDirName)) {
oldChapterDirs = sourceDir.mangaDirs[oldMangaDirName]?.chapterDirs
sourceDir.mangaDirs -= oldMangaDirName
}
// Retrieve/create the cached manga directory for new name
val newMangaDirName = provider.getMangaDirName(newTitle)
var mangaDir = sourceDir.mangaDirs[newMangaDirName]
if (mangaDir == null) {
mangaDir = MangaDirectory(mangaUniFile)
sourceDir.mangaDirs += newMangaDirName to mangaDir
}
// Add the old chapters to new name's cache
if (!oldChapterDirs.isNullOrEmpty()) {
mangaDir.chapterDirs += oldChapterDirs
}
}
notifyChanges()
}
suspend fun removeSource(source: Source) { suspend fun removeSource(source: Source) {
rootDownloadsDirMutex.withLock { rootDownloadsDirMutex.withLock {
rootDownloadsDir.sourceDirs -= source.id rootDownloadsDir.sourceDirs -= source.id
@@ -179,7 +179,7 @@ class DownloadManager(
return files.sortedBy { it.name } return files.sortedBy { it.name }
.mapIndexed { i, file -> .mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.State.READY } Page(i, uri = file.uri).apply { status = Page.State.Ready }
} }
} }
@@ -316,10 +316,13 @@ class DownloadManager(
if (removeNonFavorite && !manga.favorite) { if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source)
cleaned += 1 + mangaFolder.listFiles().orEmpty().size .getOrNull()
mangaFolder.delete() if (mangaFolder != null) {
cache.removeManga(manga) cleaned += 1 + mangaFolder.listFiles().orEmpty().size
return cleaned mangaFolder.delete()
cache.removeManga(manga)
return cleaned
}
} }
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
@@ -336,8 +339,8 @@ class DownloadManager(
} }
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) val mangaFolder = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrNull()
if (!mangaFolder.listFiles().isNullOrEmpty()) { if (mangaFolder != null && !mangaFolder.listFiles().isNullOrEmpty()) {
mangaFolder.delete() mangaFolder.delete()
cache.removeManga(manga) cache.removeManga(manga)
} else { } else {
@@ -395,6 +398,38 @@ class DownloadManager(
} }
} }
/**
* Renames manga download folder
*
* @param manga the manga
* @param newTitle the new manga title.
*/
suspend fun renameManga(manga: Manga, newTitle: String) {
val source = sourceManager.getOrStub(manga.source)
val oldFolder = provider.findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return
val newName = provider.getMangaDirName(newTitle)
if (oldFolder.name == newName) return
// just to be safe, don't allow downloads for this manga while renaming it
downloader.removeFromQueue(manga)
val capitalizationChanged = oldFolder.name.equals(newName, ignoreCase = true)
if (capitalizationChanged) {
val tempName = newName + Downloader.TMP_DIR_SUFFIX
if (!oldFolder.renameTo(tempName)) {
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
return
}
}
if (oldFolder.renameTo(newName)) {
cache.renameManga(manga, oldFolder, newTitle)
} else {
logcat(LogPriority.ERROR) { "Failed to rename manga download folder: ${oldFolder.name}" }
}
}
/** /**
* Renames an already downloaded chapter * Renames an already downloaded chapter
* *
@@ -405,7 +440,10 @@ class DownloadManager(
*/ */
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) { suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator) val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) val mangaDir = provider.getMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source).getOrElse { e ->
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
return
}
// Assume there's only 1 version of the chapter name formats present // Assume there's only 1 version of the chapter name formats present
val oldDownload = oldNames.asSequence() val oldDownload = oldNames.asSequence()
@@ -14,6 +14,7 @@ import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.IOException
/** /**
* This class is used to provide the directories where the downloads should be saved. * This class is used to provide the directories where the downloads should be saved.
@@ -35,20 +36,36 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query. * @param mangaTitle the title of the manga to query.
* @param source the source of the manga. * @param source the source of the manga.
*/ */
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile { internal fun getMangaDir(mangaTitle: String, source: Source): Result<UniFile> {
try { val downloadsDir = downloadsDir
return downloadsDir!! if (downloadsDir == null) {
.createDirectory(getSourceDirName(source))!! logcat(LogPriority.ERROR) { "Failed to create download directory" }
.createDirectory(getMangaDirName(mangaTitle))!! return Result.failure(
} catch (e: Throwable) { IOException(context.stringResource(MR.strings.storage_failed_to_create_download_directory)),
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
throw Exception(
context.stringResource(
MR.strings.invalid_location,
downloadsDir?.displayablePath ?: "",
),
) )
} }
val sourceDirName = getSourceDirName(source)
val sourceDir = downloadsDir.createDirectory(sourceDirName)
if (sourceDir == null) {
val displayablePath = downloadsDir.displayablePath + "/$sourceDirName"
logcat(LogPriority.ERROR) { "Failed to create source download directory: $displayablePath" }
return Result.failure(
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
)
}
val mangaDirName = getMangaDirName(mangaTitle)
val mangaDir = sourceDir.createDirectory(mangaDirName)
if (mangaDir == null) {
val displayablePath = sourceDir.displayablePath + "/$mangaDirName"
logcat(LogPriority.ERROR) { "Failed to create manga download directory: $displayablePath" }
return Result.failure(
IOException(context.stringResource(MR.strings.storage_failed_to_create_directory, displayablePath)),
)
}
return Result.success(mangaDir)
} }
/** /**
@@ -327,7 +327,11 @@ class Downloader(
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private suspend fun downloadChapter(download: Download) { private suspend fun downloadChapter(download: Download) {
val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source) val mangaDir = provider.getMangaDir(/* SY --> */ download.manga.ogTitle /* SY <-- */, download.source).getOrElse { e ->
download.status = Download.State.ERROR
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
return
}
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir) val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) { if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
@@ -379,11 +383,11 @@ class Downloader(
flow { flow {
// Fetch image URL if necessary // Fetch image URL if necessary
if (page.imageUrl.isNullOrEmpty()) { if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE page.status = Page.State.LoadPage
try { try {
page.imageUrl = download.source.getImageUrl(page) page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) { } catch (e: Throwable) {
page.status = Page.State.ERROR page.status = Page.State.Error(e)
} }
} }
@@ -469,12 +473,12 @@ class Downloader(
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
page.status = Page.State.READY page.status = Page.State.Ready
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
page.progress = 0 page.progress = 0
page.status = Page.State.ERROR page.status = Page.State.Error(e)
notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id) notifier.onError(e.message, download.chapter.name, download.manga.title, download.manga.id)
} }
} }
@@ -494,7 +498,7 @@ class Downloader(
filename: String, filename: String,
dataSaver: DataSaver, dataSaver: DataSaver,
): UniFile { ): UniFile {
page.status = Page.State.DOWNLOAD_IMAGE page.status = Page.State.DownloadImage
page.progress = 0 page.progress = 0
return flow { return flow {
val response = source.getImage(page, dataSaver) val response = source.getImage(page, dataSaver)
@@ -29,7 +29,7 @@ data class Download(
get() = pages?.sumOf(Page::progress) ?: 0 get() = pages?.sumOf(Page::progress) ?: 0
val downloadedImages: Int val downloadedImages: Int
get() = pages?.count { it.status == Page.State.READY } ?: 0 get() = pages?.count { it.status == Page.State.Ready } ?: 0
@Transient @Transient
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED) private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)
@@ -101,9 +101,12 @@ import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
@OptIn(ExperimentalAtomicApi::class)
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@@ -343,7 +346,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*/ */
private suspend fun updateChapterList() { private suspend fun updateChapterList() {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>() val newUpdates = CopyOnWriteArrayList<Pair<Manga, Array<Chapter>>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
@@ -408,7 +411,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (chaptersToDownload.isNotEmpty()) { if (chaptersToDownload.isNotEmpty()) {
downloadChapters(manga, chaptersToDownload) downloadChapters(manga, chaptersToDownload)
hasDownloads.set(true) hasDownloads.store(true)
} }
libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size } libraryPreferences.newUpdatesCount().getAndSet { it + newChapters.size }
@@ -441,7 +444,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates) notifier.showUpdateNotifications(newUpdates)
if (hasDownloads.get()) { if (hasDownloads.load()) {
downloadManager.startDownloads() downloadManager.startDownloads()
} }
} }
@@ -507,7 +510,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateCovers() { private suspend fun updateCovers() {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope { coroutineScope {
@@ -582,7 +585,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
var dbManga = getManga.await(networkManga.url, mangaDex.id) var dbManga = getManga.await(networkManga.url, mangaDex.id)
if (dbManga == null) { if (dbManga == null) {
dbManga = networkToLocalManga.await( dbManga = networkToLocalManga(
Manga.create().copy( Manga.create().copy(
url = networkManga.url, url = networkManga.url,
ogTitle = networkManga.title, ogTitle = networkManga.title,
@@ -641,7 +644,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInt,
manga: Manga, manga: Manga,
block: suspend () -> Unit, block: suspend () -> Unit,
) = coroutineScope { ) = coroutineScope {
@@ -650,7 +653,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
updatingManga.add(manga) updatingManga.add(manga)
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
@@ -659,10 +662,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
ensureActive() ensureActive()
updatingManga.remove(manga) updatingManga.remove(manga)
completed.getAndIncrement() completed.incrementAndFetch()
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
} }
@@ -37,8 +37,11 @@ import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.fetchAndIncrement
@OptIn(ExperimentalAtomicApi::class)
class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) : class MetadataUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@@ -97,7 +100,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private suspend fun updateMetadata() { private suspend fun updateMetadata() {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInt(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>() val currentlyUpdatingManga = CopyOnWriteArrayList<Manga>()
coroutineScope { coroutineScope {
@@ -142,7 +145,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInt,
manga: Manga, manga: Manga,
block: suspend () -> Unit, block: suspend () -> Unit,
) = coroutineScope { ) = coroutineScope {
@@ -151,7 +154,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
updatingManga.add(manga) updatingManga.add(manga)
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
@@ -160,10 +163,10 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
ensureActive() ensureActive()
updatingManga.remove(manga) updatingManga.remove(manga)
completed.getAndIncrement() completed.fetchAndIncrement()
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.load(),
mangaToUpdate.size, mangaToUpdate.size,
) )
} }
@@ -602,18 +602,17 @@ class NotificationReceiver : BroadcastReceiver() {
} }
/** /**
* Returns [PendingIntent] that starts a share activity for a backup file. * Returns [PendingIntent] that directly launches a share activity for a backup file.
* *
* @param context context of application * @param context context of application
* @param uri uri of backup file * @param uri uri of backup file
* @return [PendingIntent] * @return [PendingIntent]
*/ */
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri): PendingIntent { internal fun shareBackupPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply { val intent = uri.toShareIntent(context, "application/x-protobuf+gzip").apply {
action = ACTION_SHARE_BACKUP addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(EXTRA_URI, uri)
} }
return PendingIntent.getBroadcast( return PendingIntent.getActivity(
context, context,
0, 0,
intent, intent,
@@ -108,6 +108,7 @@ class BangumiApi(
.awaitSuccess() .awaitSuccess()
.parseAs<BGMSearchResult>() .parseAs<BGMSearchResult>()
.data .data
.filter { it.platform == null || it.platform == "漫画" }
.map { it.toTrackSearch(trackId) } .map { it.toTrackSearch(trackId) }
} }
} }
@@ -25,6 +25,7 @@ data class BGMSubject(
val volumes: Long = 0, val volumes: Long = 0,
val eps: Long = 0, val eps: Long = 0,
val rating: BGMSubjectRating?, val rating: BGMSubjectRating?,
val platform: String?,
// SY --> // SY -->
val infobox: List<Infobox> = emptyList(), val infobox: List<Infobox> = emptyList(),
// SY <-- // SY <--
@@ -12,16 +12,18 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicReference import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
/** /**
* Base implementation class for extension installer. To be used inside a foreground [Service]. * Base implementation class for extension installer. To be used inside a foreground [Service].
*/ */
@OptIn(ExperimentalAtomicApi::class)
abstract class Installer(private val service: Service) { abstract class Installer(private val service: Service) {
private val extensionManager: ExtensionManager by injectLazy() private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null) private var waitingInstall = AtomicReference<Entry?>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>()) private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() { private val cancelReceiver = object : BroadcastReceiver() {
@@ -79,7 +81,7 @@ abstract class Installer(private val service: Service) {
* @see waitingInstall * @see waitingInstall
*/ */
fun continueQueue(resultStep: InstallStep) { fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null) val completedEntry = waitingInstall.exchange(null)
if (completedEntry != null) { if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep) extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue() checkQueue()
@@ -115,10 +117,10 @@ abstract class Installer(private val service: Service) {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver) LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) } queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.clear() queue.clear()
waitingInstall.set(null) waitingInstall.store(null)
} }
protected fun getActiveEntry(): Entry? = waitingInstall.get() protected fun getActiveEntry(): Entry? = waitingInstall.load()
/** /**
* Cancels queue for the provided download ID if exists. * Cancels queue for the provided download ID if exists.
@@ -126,13 +128,13 @@ abstract class Installer(private val service: Service) {
* @param downloadId Download ID as known by [ExtensionManager] * @param downloadId Download ID as known by [ExtensionManager]
*/ */
private fun cancelQueue(downloadId: Long) { private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get() val waitingInstall = this.waitingInstall.load()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) { if (cancelEntry(toCancel)) {
queue.remove(toCancel) queue.remove(toCancel)
if (waitingInstall == toCancel) { if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue // Currently processing removed entry, continue queue
this.waitingInstall.set(null) this.waitingInstall.store(null)
checkQueue() checkQueue()
} }
extensionManager.updateInstallStep(downloadId, InstallStep.Idle) extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
@@ -170,7 +170,7 @@ class MergedSource : HttpSource() {
var manga = getManga.await(mangaUrl, mangaSourceId) var manga = getManga.await(mangaUrl, mangaSourceId)
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId) val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
if (manga == null) { if (manga == null) {
val newManga = networkToLocalManga.await( val newManga = networkToLocalManga(
Manga.create().copy( Manga.create().copy(
source = mangaSourceId, source = mangaSourceId,
url = mangaUrl, url = mangaUrl,
@@ -7,7 +7,6 @@ import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.FeedItemUI import eu.kanade.presentation.browse.FeedItemUI
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -251,9 +251,7 @@ open class FeedScreenModel(
val result = withIOContext { val result = withIOContext {
itemUI.copy( itemUI.copy(
results = page.map { results = networkToLocalManga(page.map { it.toDomainManga(itemUI.source!!.id) }),
networkToLocalManga.await(it.toDomainManga(itemUI.source!!.id))
},
) )
} }
@@ -8,6 +8,7 @@ object MigrationFlags {
const val CUSTOM_COVER = 0b01000 const val CUSTOM_COVER = 0b01000
const val EXTRA = 0b10000 const val EXTRA = 0b10000
const val DELETE_CHAPTERS = 0b100000 const val DELETE_CHAPTERS = 0b100000
const val NOTES = 0b1000000
fun hasChapters(value: Int): Boolean { fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0 return value and CHAPTERS != 0
@@ -32,4 +33,8 @@ object MigrationFlags {
fun hasDeleteChapters(value: Int): Boolean { fun hasDeleteChapters(value: Int): Boolean {
return value and DELETE_CHAPTERS != 0 return value and DELETE_CHAPTERS != 0
} }
fun hasNotes(value: Int): Boolean {
return value and NOTES != 0
}
} }
@@ -59,6 +59,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags) binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags) binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags) binding.migDeleteDownloaded.isChecked = MigrationFlags.hasDeleteChapters(flags)
binding.migNotes.isChecked = MigrationFlags.hasNotes(flags)
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
@@ -66,6 +67,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) } binding.migDeleteDownloaded.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.migNotes.setOnCheckedChangeListener { _, _ -> setFlags(binding) }
binding.useSmartSearch.bindToPreference(preferences.smartMigration()) binding.useSmartSearch.bindToPreference(preferences.smartMigration())
binding.extraSearchParamText.isVisible = false binding.extraSearchParamText.isVisible = false
@@ -108,6 +110,7 @@ class MigrationBottomSheetDialogState(private val onStartMigration: State<(extra
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS if (binding.migDeleteDownloaded.isChecked) flags = flags or MigrationFlags.DELETE_CHAPTERS
if (binding.migNotes.isChecked) flags = flags or MigrationFlags.NOTES
preferences.migrateFlags().set(flags) preferences.migrateFlags().set(flags)
} }
@@ -226,7 +226,7 @@ class MigrationListScreenModel(
if (searchResult != null && if (searchResult != null &&
!(searchResult.url == mangaObj.url && source.id == mangaObj.source) !(searchResult.url == mangaObj.url && source.id == mangaObj.source)
) { ) {
val localManga = networkToLocalManga.await(searchResult) val localManga = networkToLocalManga(searchResult)
val chapters = if (source is EHentai) { val chapters = if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle) source.getChapterList(localManga.toSManga(), throttleManager::throttle)
@@ -264,7 +264,7 @@ class MigrationListScreenModel(
} }
if (searchResult != null) { if (searchResult != null) {
val localManga = networkToLocalManga.await(searchResult) val localManga = networkToLocalManga(searchResult)
val chapters = try { val chapters = try {
if (source is EHentai) { if (source is EHentai) {
source.getChapterList(localManga.toSManga(), throttleManager::throttle) source.getChapterList(localManga.toSManga(), throttleManager::throttle)
@@ -455,7 +455,7 @@ class MigrationListScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val result = migratingManga.migrationScope.async { val result = migratingManga.migrationScope.async {
val manga = getManga(newMangaId)!! val manga = getManga(newMangaId)!!
val localManga = networkToLocalManga.await(manga) val localManga = networkToLocalManga(manga)
try { try {
val source = sourceManager.get(manga.source)!! val source = sourceManager.get(manga.source)!!
val chapters = source.getChapterList(localManga.toSManga()) val chapters = source.getChapterList(localManga.toSManga())
@@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@@ -150,7 +151,11 @@ data class BrowseSourceScreen(
Scaffold( Scaffold(
topBar = { topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.pointerInput(Unit) {},
) {
BrowseSourceToolbar( BrowseSourceToolbar(
searchQuery = state.toolbarQuery, searchQuery = state.toolbarQuery,
onSearchQueryChange = screenModel::setToolbarQuery, onSearchQueryChange = screenModel::setToolbarQuery,
@@ -256,14 +261,11 @@ data class BrowseSourceScreen(
onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) }, onMangaClick = { navigator.push(MangaScreen(it.id, true, smartSearchConfig)) },
onMangaLongClick = { manga -> onMangaLongClick = { manga ->
scope.launchIO { scope.launchIO {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga) val duplicates = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog( duplicates.isNotEmpty() -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga( BrowseSourceScreenModel.Dialog.AddDuplicateManga(manga, duplicates),
manga,
duplicateManga,
),
) )
else -> screenModel.addFavorite(manga) else -> screenModel.addFavorite(manga)
} }
@@ -318,15 +320,16 @@ data class BrowseSourceScreen(
} }
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) }, onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
// SY <-- // SY <--
@@ -16,7 +16,6 @@ import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.interactor.GetExhSavedSearch import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetIncognitoState import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@@ -50,7 +48,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.CheckboxState
@@ -67,15 +64,15 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFlatMetadataById import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.toMangaUpdate import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.interactor.DeleteSavedSearchById import tachiyomi.domain.source.interactor.DeleteSavedSearchById
import tachiyomi.domain.source.interactor.GetRemoteManga import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.interactor.InsertSavedSearch import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.model.EXHSavedSearch import tachiyomi.domain.source.model.EXHSavedSearch
import tachiyomi.domain.source.model.SavedSearch import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.repository.SourcePagingSourceType import tachiyomi.domain.source.repository.SourcePagingSource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -101,7 +98,6 @@ open class BrowseSourceScreenModel(
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(), private val setMangaDefaultChapterFlags: SetMangaDefaultChapterFlags = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(), private val addTracks: AddTracks = Injekt.get(),
private val getIncognitoState: GetIncognitoState = Injekt.get(), private val getIncognitoState: GetIncognitoState = Injekt.get(),
@@ -193,10 +189,9 @@ open class BrowseSourceScreenModel(
createSourcePagingSource(listing.query ?: "", listing.filters) createSourcePagingSource(listing.query ?: "", listing.filters)
// SY <-- // SY <--
}.flow.map { pagingData -> }.flow.map { pagingData ->
pagingData.map { (it, metadata) -> pagingData.map { (manga, metadata) ->
networkToLocalManga.await(it.toDomainManga(sourceId)) getManga.subscribe(manga.url, manga.source)
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) } .map { it ?: manga }
.filterNotNull()
// SY --> // SY -->
.combineMetadata(metadata) .combineMetadata(metadata)
// SY <-- // SY <--
@@ -382,8 +377,8 @@ open class BrowseSourceScreenModel(
} }
// SY --> // SY -->
open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType { open fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSource {
return getRemoteManga.subscribe(sourceId, query, filters) return getRemoteManga(sourceId, query, filters)
} }
// SY <-- // SY <--
@@ -399,8 +394,8 @@ open class BrowseSourceScreenModel(
.orEmpty() .orEmpty()
} }
suspend fun getDuplicateLibraryManga(manga: Manga): Manga? { suspend fun getDuplicateLibraryManga(manga: Manga): List<MangaWithChapterCount> {
return getDuplicateLibraryManga.await(manga).getOrNull(0) return getDuplicateLibraryManga.invoke(manga)
} }
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) { private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
@@ -450,7 +445,7 @@ open class BrowseSourceScreenModel(
sealed interface Dialog { sealed interface Dialog {
data object Filter : Dialog data object Filter : Dialog
data class RemoveManga(val manga: Manga) : Dialog data class RemoveManga(val manga: Manga) : Dialog
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class AddDuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
data class ChangeMangaCategory( data class ChangeMangaCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState.State<Category>>, val initialSelection: ImmutableList<CheckboxState.State<Category>>,
@@ -188,22 +188,24 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit/* SY --> */, star
) { ) {
Column { Column {
filter.values.mapIndexed { index, item -> filter.values.mapIndexed { index, item ->
val sortAscending = filter.state?.ascending
?.takeIf { index == filter.state?.index }
SortItem( SortItem(
label = item, label = item,
sortDescending = filter.state?.ascending?.not() sortDescending = if (sortAscending != null) !sortAscending else null,
?.takeIf { index == filter.state?.index }, onClick = {
) { val ascending = if (index == filter.state?.index) {
val ascending = if (index == filter.state?.index) { !filter.state!!.ascending
!filter.state!!.ascending } else {
} else { filter.state?.ascending ?: true
filter.state!!.ascending }
} filter.state = Filter.Sort.Selection(
filter.state = Filter.Sort.Selection( index = index,
index = index, ascending = ascending,
ascending = ascending, )
) onUpdate()
onUpdate() },
} )
} }
} }
} }
@@ -10,7 +10,6 @@ import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.interactor.GetExhSavedSearch import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.browse.SourceFeedUI import eu.kanade.presentation.browse.SourceFeedUI
@@ -32,8 +31,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
@@ -173,9 +172,7 @@ open class SourceFeedScreenModel(
} }
val titles = withIOContext { val titles = withIOContext {
page.map { networkToLocalManga(page.map { it.toDomainManga(source.id) })
networkToLocalManga.await(it.toDomainManga(source.id))
}
} }
mutableState.update { state -> mutableState.update { state ->
@@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
@@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.preference.toggle import tachiyomi.core.common.preference.toggle
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
@@ -169,9 +169,10 @@ abstract class SearchScreenModel(
source.getSearchManga(1, query, source.getFilterList()) source.getSearchManga(1, query, source.getFilterList())
} }
val titles = page.mangas.map { val titles = page.mangas
networkToLocalManga.await(it.toDomainManga(source.id)) .map { it.toDomainManga(source.id) }
} .distinctBy { it.url }
.let { networkToLocalManga(it) }
if (isActive) { if (isActive) {
updateItem(source, SearchItemResult.Success(titles)) updateItem(source, SearchItemResult.Success(titles))
@@ -4,18 +4,16 @@ import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -27,7 +25,6 @@ class DeepLinkScreenModel(
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(), private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(), private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) { ) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
@@ -38,7 +35,7 @@ class DeepLinkScreenModel(
.firstOrNull { it.getUriType(query) != UriType.Unknown } .firstOrNull { it.getUriType(query) != UriType.Unknown }
val manga = source?.getManga(query)?.let { val manga = source?.getManga(query)?.let {
getMangaFromSManga(it, source.id) networkToLocalManga(it.toDomainManga(source.id))
} }
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) { val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
@@ -73,11 +70,6 @@ class DeepLinkScreenModel(
} }
} }
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
return getMangaByUrlAndSourceId.await(sManga.url, sourceId)
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
}
sealed interface State { sealed interface State {
@Immutable @Immutable
data object Loading : State data object Loading : State
@@ -40,6 +40,7 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -174,9 +175,9 @@ class HistoryScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
val manga = getManga.await(mangaId) ?: return@launchIO val manga = getManga.await(mangaId) ?: return@launchIO
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) val duplicates = getDuplicateLibraryManga(manga)
if (duplicate != null) { if (duplicates.isNotEmpty()) {
mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
return@launchIO return@launchIO
} }
@@ -246,7 +247,7 @@ class HistoryScreenModel(
sealed interface Dialog { sealed interface Dialog {
data object DeleteAll : Dialog data object DeleteAll : Dialog
data class Delete(val history: HistoryWithRelations) : Dialog data class Delete(val history: HistoryWithRelations) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
data class ChangeCategory( data class ChangeCategory(
val manga: Manga, val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
@@ -114,17 +114,18 @@ data object HistoryTab : Tab {
} }
is HistoryScreenModel.Dialog.DuplicateManga -> { is HistoryScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { onConfirm = {
screenModel.addFavorite(dialog.manga) screenModel.addFavorite(dialog.manga)
}, },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
// SY <-- // SY <--
@@ -148,7 +149,7 @@ data object HistoryTab : Tab {
screenModel = MigrateDialogScreenModel(), screenModel = MigrateDialogScreenModel(),
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) }, onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
onPopScreen = { navigator.replace(MangaScreen(dialog.newManga.id)) }, onPopScreen = onDismissRequest,
) )
} SY <--*/ } SY <--*/
null -> {} null -> {}
@@ -1,20 +1,21 @@
package eu.kanade.tachiyomi.ui.home package eu.kanade.tachiyomi.ui.home
import androidx.activity.compose.BackHandler import android.annotation.SuppressLint
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItem
@@ -23,15 +24,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.lerp
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
@@ -53,6 +63,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import soup.compose.material.motion.MotionConstants
import soup.compose.material.motion.animation.materialFadeThroughIn import soup.compose.material.motion.animation.materialFadeThroughIn
import soup.compose.material.motion.animation.materialFadeThroughOut import soup.compose.material.motion.animation.materialFadeThroughOut
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
@@ -61,8 +72,10 @@ import tachiyomi.presentation.core.components.material.NavigationBar
import tachiyomi.presentation.core.components.material.NavigationRail import tachiyomi.presentation.core.components.material.NavigationRail
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.util.PredictiveBack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.coroutines.cancellation.CancellationException
object HomeScreen : Screen() { object HomeScreen : Screen() {
@@ -70,8 +83,14 @@ object HomeScreen : Screen() {
private val openTabEvent = Channel<Tab>() private val openTabEvent = Channel<Tab>()
private val showBottomNavEvent = Channel<Boolean>() private val showBottomNavEvent = Channel<Boolean>()
private const val TAB_FADE_DURATION = 200 @Suppress("ConstPropertyName")
private const val TAB_NAVIGATOR_KEY = "HomeTabs" private const val TabFadeDuration = 200
@Suppress("ConstPropertyName")
private const val TabNavigatorKey = "HomeTabs"
@SuppressLint("ComposeCompositionLocalUsage")
val LocalHomeScreenInsetsProvider = staticCompositionLocalOf { WindowInsets(0.dp) }
private val TABS = listOf( private val TABS = listOf(
LibraryTab, LibraryTab,
@@ -84,6 +103,7 @@ object HomeScreen : Screen() {
@Composable @Composable
override fun Content() { override fun Content() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
var scale by remember { mutableFloatStateOf(1f) }
// SY --> // SY -->
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -94,7 +114,7 @@ object HomeScreen : Screen() {
TabNavigator( TabNavigator(
tab = LibraryTab, tab = LibraryTab,
key = TAB_NAVIGATOR_KEY, key = TabNavigatorKey,
) { tabNavigator -> ) { tabNavigator ->
// Provide usable navigator to content screen // Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) { CompositionLocalProvider(LocalNavigator provides navigator) {
@@ -134,26 +154,62 @@ object HomeScreen : Screen() {
} }
} }
}, },
contentWindowInsets = WindowInsets(0),
) { contentPadding -> ) { contentPadding ->
Box( Box(
modifier = Modifier modifier = Modifier
.padding(contentPadding) .graphicsLayer {
.consumeWindowInsets(contentPadding), scaleX = scale
scaleY = scale
}
.windowInsetsPadding(
remember {
object : WindowInsets {
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int {
return with(density) {
contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
}
}
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int {
return with(density) {
contentPadding.calculateRightPadding(layoutDirection).roundToPx()
}
}
override fun getBottom(density: Density): Int = 0
override fun getTop(density: Density): Int = 0
}
},
),
) { ) {
val insets = remember {
object : WindowInsets {
override fun getBottom(density: Density): Int {
return with(density) { contentPadding.calculateBottomPadding().roundToPx() }
}
override fun getTop(density: Density): Int {
return with(density) { contentPadding.calculateTopPadding().roundToPx() }
}
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0
}
}
AnimatedContent( AnimatedContent(
targetState = tabNavigator.current, targetState = tabNavigator.current,
transitionSpec = { transitionSpec = {
materialFadeThroughIn( materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
initialScale = 1f, materialFadeThroughOut(durationMillis = TabFadeDuration)
durationMillis = TAB_FADE_DURATION,
) togetherWith
materialFadeThroughOut(durationMillis = TAB_FADE_DURATION)
}, },
label = "tabContent", label = "tabContent",
) { ) {
tabNavigator.saveableState(key = "currentTab", it) { CompositionLocalProvider(LocalHomeScreenInsetsProvider provides insets) {
it.Content() tabNavigator.saveableState(key = "currentTab", it) {
it.Content()
}
} }
} }
} }
@@ -161,10 +217,32 @@ object HomeScreen : Screen() {
} }
val goToLibraryTab = { tabNavigator.current = LibraryTab } val goToLibraryTab = { tabNavigator.current = LibraryTab }
BackHandler(
enabled = tabNavigator.current != LibraryTab, var handlingBack by remember { mutableStateOf(false) }
onBack = goToLibraryTab, PredictiveBackHandler(
) enabled = handlingBack || tabNavigator.current::class != LibraryTab::class,
) { progress ->
handlingBack = true
val currentTab = tabNavigator.current
try {
progress.collect { backEvent ->
scale = lerp(1f, 0.92f, PredictiveBack.transform(backEvent.progress))
tabNavigator.current = if (backEvent.progress > 0.25f) TABS[0] else currentTab
}
goToLibraryTab()
} catch (e: CancellationException) {
tabNavigator.current = currentTab
} finally {
animate(
initialValue = scale,
targetValue = 1f,
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
) { value, _ ->
scale = value
}
handlingBack = false
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch { launch {
@@ -312,8 +390,6 @@ object HomeScreen : Screen() {
Icon( Icon(
painter = tab.options.icon!!, painter = tab.options.icon!!,
contentDescription = tab.options.title, contentDescription = tab.options.title,
// TODO: https://issuetracker.google.com/u/0/issues/316327367
tint = LocalContentColor.current,
) )
} }
} }
@@ -51,6 +51,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog
import eu.kanade.tachiyomi.ui.manga.notes.MangaNotesScreen
import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen import eu.kanade.tachiyomi.ui.manga.track.TrackInfoDialogHomeScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.setting.SettingsScreen import eu.kanade.tachiyomi.ui.setting.SettingsScreen
@@ -205,6 +206,7 @@ class MangaScreen(
successState.manga.favorite successState.manga.favorite
}, },
previewsRowCount = successState.previewsRowCount, previewsRowCount = successState.previewsRowCount,
onEditNotesClicked = { navigator.push(MangaNotesScreen(manga = successState.manga)) },
// SY --> // SY -->
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite }, onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) }, onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
@@ -259,12 +261,13 @@ class MangaScreen(
is MangaScreenModel.Dialog.DuplicateManga -> { is MangaScreenModel.Dialog.DuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) }, onConfirm = { screenModel.toggleFavorite(onRemoved = {}, checkDuplicate = false) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
// SY --> // SY -->
migrateManga(navigator, dialog.duplicate, screenModel.manga!!.id) migrateManga(navigator, it, screenModel.manga!!.id)
// SY <-- // SY <--
}, },
) )
@@ -125,6 +125,7 @@ import tachiyomi.domain.manga.interactor.UpdateMergedSettings
import tachiyomi.domain.manga.model.CustomMangaInfo import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
import tachiyomi.domain.manga.model.MergedMangaReference import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.manga.model.applyFilter
@@ -658,7 +659,7 @@ class MangaScreenModel(
existingManga = getManga.await(mergedManga.url, mergedManga.source) existingManga = getManga.await(mergedManga.url, mergedManga.source)
} }
mergedManga = networkToLocalManga.await(mergedManga) mergedManga = networkToLocalManga(mergedManga)
getCategories.await(originalMangaId) getCategories.await(originalMangaId)
.let { .let {
@@ -783,10 +784,10 @@ class MangaScreenModel(
// Add to library // Add to library
// First, check if duplicate exists if callback is provided // First, check if duplicate exists if callback is provided
if (checkDuplicate) { if (checkDuplicate) {
val duplicate = getDuplicateLibraryManga.await(manga).getOrNull(0) val duplicates = getDuplicateLibraryManga(manga)
if (duplicate != null) { if (duplicates.isNotEmpty()) {
updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicate)) } updateSuccessState { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) }
return@launchIO return@launchIO
} }
} }
@@ -1653,7 +1654,7 @@ class MangaScreenModel(
val initialSelection: ImmutableList<CheckboxState<Category>>, val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog ) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
/* SY --> /* SY -->
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
@@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.ui.manga.notes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.manga.MangaNotesScreen
import eu.kanade.presentation.util.Screen
import kotlinx.coroutines.flow.update
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.manga.interactor.UpdateMangaNotes
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaNotesScreen(
private val manga: Manga,
) : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { Model(manga) }
val state by screenModel.state.collectAsState()
MangaNotesScreen(
state = state,
navigateUp = navigator::pop,
onUpdate = screenModel::updateNotes,
)
}
private class Model(
private val manga: Manga,
private val updateMangaNotes: UpdateMangaNotes = Injekt.get(),
) : StateScreenModel<State>(State(manga, manga.notes)) {
fun updateNotes(content: String) {
if (content == state.value.notes) return
mutableState.update {
it.copy(notes = content)
}
screenModelScope.launchNonCancellable {
updateMangaNotes(manga.id, content)
}
}
}
@Immutable
data class State(
val manga: Manga,
val notes: String,
)
}
@@ -33,12 +33,9 @@ class OnboardingScreen : Screen() {
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString) val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
BackHandler( BackHandler(enabled = !shownOnboardingFlow) {
enabled = !shownOnboardingFlow, // Prevent exiting if onboarding hasn't been completed
onBack = { }
// Prevent exiting if onboarding hasn't been completed
},
)
OnboardingScreen( OnboardingScreen(
onComplete = finishOnboarding, onComplete = finishOnboarding,
@@ -717,7 +717,7 @@ class ReaderActivity : BaseActivity() {
?.pages ?.pages
?.forEachIndexed { _, page -> ?.forEachIndexed { _, page ->
var shouldQueuePage = false var shouldQueuePage = false
if (page.status == Page.State.ERROR) { if (page.status is Page.State.Error) {
shouldQueuePage = true shouldQueuePage = true
} /*else if (page.status == Page.LOAD_PAGE || } /*else if (page.status == Page.LOAD_PAGE ||
page.status == Page.DOWNLOAD_IMAGE) { page.status == Page.DOWNLOAD_IMAGE) {
@@ -725,7 +725,7 @@ class ReaderActivity : BaseActivity() {
}*/ }*/
if (shouldQueuePage) { if (shouldQueuePage) {
page.status = Page.State.QUEUE page.status = Page.State.Queue
} else { } else {
return@forEachIndexed return@forEachIndexed
} }
@@ -758,11 +758,11 @@ class ReaderActivity : BaseActivity() {
return return
} }
if (curPage.status == Page.State.ERROR) { if (curPage.status is Page.State.Error) {
toast(SYMR.strings.eh_boost_page_errored) toast(SYMR.strings.eh_boost_page_errored)
} else if (curPage.status == Page.State.LOAD_PAGE || curPage.status == Page.State.DOWNLOAD_IMAGE) { } else if (curPage.status == Page.State.LoadPage || curPage.status == Page.State.DownloadImage) {
toast(SYMR.strings.eh_boost_page_downloading) toast(SYMR.strings.eh_boost_page_downloading)
} else if (curPage.status == Page.State.READY) { } else if (curPage.status == Page.State.Ready) {
toast(SYMR.strings.eh_boost_page_downloaded) toast(SYMR.strings.eh_boost_page_downloaded)
} else { } else {
val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader) val loader = (viewModel.state.value.viewerChapters?.currChapter?.pageLoader as? HttpPageLoader)
@@ -19,7 +19,6 @@ import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.data.database.models.isRecognizedNumber
import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
@@ -184,6 +183,11 @@ class ReaderViewModel @JvmOverloads constructor(
private var chapterToDownload: Download? = null private var chapterToDownload: Download? = null
private val unfilteredChapterList by lazy {
val manga = manga!!
runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) }
}
/** /**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI. * time in a background thread to avoid blocking the UI.
@@ -693,7 +697,7 @@ class ReaderViewModel @JvmOverloads constructor(
readerChapter.requestedPage = pageIndex readerChapter.requestedPage = pageIndex
chapterPageIndex = pageIndex chapterPageIndex = pageIndex
if (!incognitoMode && page.status != Page.State.ERROR) { if (!incognitoMode && page.status !is Page.State.Error) {
readerChapter.chapter.last_page_read = pageIndex readerChapter.chapter.last_page_read = pageIndex
// SY --> // SY -->
@@ -732,11 +736,11 @@ class ReaderViewModel @JvmOverloads constructor(
// SY --> // SY -->
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val chapterUpdates = chapterList val chapterUpdates = unfilteredChapterList
.filter { it.chapter.source_order > readerChapter.chapter.source_order } .filter { it.sourceOrder > readerChapter.chapter.source_order }
.map { chapter -> .map { chapter ->
ChapterUpdate( ChapterUpdate(
id = chapter.chapter.id!!, id = chapter.id,
read = true, read = true,
) )
} }
@@ -752,15 +756,14 @@ class ReaderViewModel @JvmOverloads constructor(
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING) .contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING)
if (!markDuplicateAsRead) return if (!markDuplicateAsRead) return
val duplicateUnreadChapters = chapterList val duplicateUnreadChapters = unfilteredChapterList
.mapNotNull { .mapNotNull { chapter ->
val chapter = it.chapter
if ( if (
!chapter.read && !chapter.read &&
chapter.isRecognizedNumber && chapter.isRecognizedNumber &&
chapter.chapter_number == readerChapter.chapter.chapter_number chapter.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
) { ) {
ChapterUpdate(id = chapter.id!!, read = true) ChapterUpdate(id = chapter.id, read = true)
// SY --> // SY -->
.also { deleteChapterIfNeeded(ReaderChapter(chapter)) } .also { deleteChapterIfNeeded(ReaderChapter(chapter)) }
// SY <-- // SY <--
@@ -1068,7 +1071,7 @@ class ReaderViewModel @JvmOverloads constructor(
(state.value.dialog as? Dialog.PageActions)?.page (state.value.dialog as? Dialog.PageActions)?.page
} }
// SY <-- // SY <--
if (page?.status != Page.State.READY) return if (page?.status != Page.State.Ready) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@@ -1112,8 +1115,8 @@ class ReaderViewModel @JvmOverloads constructor(
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages) val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
val bg = viewer.config.pageCanvasColor val bg = viewer.config.pageCanvasColor
if (firstPage.status != Page.State.READY) return if (firstPage.status != Page.State.Ready) return
if (secondPage?.status != Page.State.READY) return if (secondPage?.status != Page.State.Ready) return
val manga = manga ?: return val manga = manga ?: return
@@ -1188,7 +1191,7 @@ class ReaderViewModel @JvmOverloads constructor(
(state.value.dialog as? Dialog.PageActions)?.page (state.value.dialog as? Dialog.PageActions)?.page
} }
// SY <-- // SY <--
if (page?.status != Page.State.READY) return if (page?.status != Page.State.Ready) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@@ -1220,8 +1223,8 @@ class ReaderViewModel @JvmOverloads constructor(
val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages) val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages)
val bg = viewer.config.pageCanvasColor val bg = viewer.config.pageCanvasColor
if (firstPage.status != Page.State.READY) return if (firstPage.status != Page.State.Ready) return
if (secondPage?.status != Page.State.READY) return if (secondPage?.status != Page.State.Ready) return
val manga = manga ?: return val manga = manga ?: return
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
@@ -1257,7 +1260,7 @@ class ReaderViewModel @JvmOverloads constructor(
(state.value.dialog as? Dialog.PageActions)?.page (state.value.dialog as? Dialog.PageActions)?.page
} }
// SY <-- // SY <--
if (page?.status != Page.State.READY) return if (page?.status != Page.State.Ready) return
val manga = manga ?: return val manga = manga ?: return
val stream = page.stream ?: return val stream = page.stream ?: return
@@ -90,7 +90,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader
// SY --> // SY -->
stream = { imageBytes?.copyOf()?.inputStream() ?: reader.getInputStream(entry.name)!! } stream = { imageBytes?.copyOf()?.inputStream() ?: reader.getInputStream(entry.name)!! }
// SY <-- // SY <--
status = Page.State.READY status = Page.State.Ready
} }
} }
.toList() .toList()
@@ -21,7 +21,7 @@ internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
val streamFn = { file.openInputStream() } val streamFn = { file.openInputStream() }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.State.READY status = Page.State.Ready
} }
} }
.orEmpty() .orEmpty()
@@ -57,7 +57,7 @@ internal class DownloadPageLoader(
ReaderPage(page.index, page.url, page.imageUrl) { ReaderPage(page.index, page.url, page.imageUrl) {
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!! context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
}.apply { }.apply {
status = Page.State.READY status = Page.State.Ready
} }
} }
} }
@@ -20,7 +20,7 @@ internal class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
val streamFn = { epub.getInputStream(path)!! } val streamFn = { epub.getInputStream(path)!! }
ReaderPage(i).apply { ReaderPage(i).apply {
stream = streamFn stream = streamFn
status = Page.State.READY status = Page.State.Ready
} }
} }
} }
@@ -26,7 +26,9 @@ import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.math.min import kotlin.math.min
/** /**
@@ -66,7 +68,7 @@ internal class HttpPageLoader(
emit(runInterruptible { queue.take() }.page) emit(runInterruptible { queue.take() }.page)
} }
} }
.filter { it.status == Page.State.QUEUE } .filter { it.status == Page.State.Queue }
.collect(::internalLoadPage) .collect(::internalLoadPage)
} }
// EXH --> // EXH -->
@@ -96,7 +98,7 @@ internal class HttpPageLoader(
} }
if (readerPreferences.aggressivePageLoading().get()) { if (readerPreferences.aggressivePageLoading().get()) {
rp.forEach { rp.forEach {
if (it.status == Page.State.QUEUE) { if (it.status == Page.State.Queue) {
queue.offer(PriorityPage(it, 0)) queue.offer(PriorityPage(it, 0))
} }
} }
@@ -112,17 +114,17 @@ internal class HttpPageLoader(
val imageUrl = page.imageUrl val imageUrl = page.imageUrl
// Check if the image has been deleted // Check if the image has been deleted
if (page.status == Page.State.READY && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) { if (page.status == Page.State.Ready && imageUrl != null && !chapterCache.isImageInCache(imageUrl)) {
page.status = Page.State.QUEUE page.status = Page.State.Queue
} }
// Automatically retry failed pages when subscribed to this page // Automatically retry failed pages when subscribed to this page
if (page.status == Page.State.ERROR) { if (page.status is Page.State.Error) {
page.status = Page.State.QUEUE page.status = Page.State.Queue
} }
val queuedPages = mutableListOf<PriorityPage>() val queuedPages = mutableListOf<PriorityPage>()
if (page.status == Page.State.QUEUE) { if (page.status == Page.State.Queue) {
queuedPages += PriorityPage(page, 1).also { queue.offer(it) } queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
} }
queuedPages += preloadNextPages(page, preloadSize) queuedPages += preloadNextPages(page, preloadSize)
@@ -130,7 +132,7 @@ internal class HttpPageLoader(
suspendCancellableCoroutine<Nothing> { continuation -> suspendCancellableCoroutine<Nothing> { continuation ->
continuation.invokeOnCancellation { continuation.invokeOnCancellation {
queuedPages.forEach { queuedPages.forEach {
if (it.page.status == Page.State.QUEUE) { if (it.page.status == Page.State.Queue) {
queue.remove(it) queue.remove(it)
} }
} }
@@ -142,8 +144,8 @@ internal class HttpPageLoader(
* Retries a page. This method is only called from user interaction on the viewer. * Retries a page. This method is only called from user interaction on the viewer.
*/ */
override fun retryPage(page: ReaderPage) { override fun retryPage(page: ReaderPage) {
if (page.status == Page.State.ERROR) { if (page.status is Page.State.Error) {
page.status = Page.State.QUEUE page.status = Page.State.Queue
} }
// EXH --> // EXH -->
// Grab a new image URL on EXH sources // Grab a new image URL on EXH sources
@@ -194,7 +196,7 @@ internal class HttpPageLoader(
return pages return pages
.subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size)) .subList(pageIndex + 1, min(pageIndex + 1 + amount, pages.size))
.mapNotNull { .mapNotNull {
if (it.status == Page.State.QUEUE) { if (it.status == Page.State.Queue) {
PriorityPage(it, 0).apply { queue.offer(this) } PriorityPage(it, 0).apply { queue.offer(this) }
} else { } else {
null null
@@ -211,21 +213,21 @@ internal class HttpPageLoader(
private suspend fun internalLoadPage(page: ReaderPage) { private suspend fun internalLoadPage(page: ReaderPage) {
try { try {
if (page.imageUrl.isNullOrEmpty()) { if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE page.status = Page.State.LoadPage
page.imageUrl = source.getImageUrl(page) page.imageUrl = source.getImageUrl(page)
} }
val imageUrl = page.imageUrl!! val imageUrl = page.imageUrl!!
if (!chapterCache.isImageInCache(imageUrl)) { if (!chapterCache.isImageInCache(imageUrl)) {
page.status = Page.State.DOWNLOAD_IMAGE page.status = Page.State.DownloadImage
val imageResponse = source.getImage(page, dataSaver) val imageResponse = source.getImage(page, dataSaver)
chapterCache.putImageToCache(imageUrl, imageResponse) chapterCache.putImageToCache(imageUrl, imageResponse)
} }
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() } page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
page.status = Page.State.READY page.status = Page.State.Ready
} catch (e: Throwable) { } catch (e: Throwable) {
page.status = Page.State.ERROR page.status = Page.State.Error(e)
if (e is CancellationException) { if (e is CancellationException) {
throw e throw e
} }
@@ -234,7 +236,7 @@ internal class HttpPageLoader(
// EXH --> // EXH -->
fun boostPage(page: ReaderPage) { fun boostPage(page: ReaderPage) {
if (page.status == Page.State.QUEUE) { if (page.status == Page.State.Queue) {
scope.launchIO { scope.launchIO {
loadPage(page) loadPage(page)
} }
@@ -246,15 +248,16 @@ internal class HttpPageLoader(
/** /**
* Data class used to keep ordering of pages in order to maintain priority. * Data class used to keep ordering of pages in order to maintain priority.
*/ */
@OptIn(ExperimentalAtomicApi::class)
private class PriorityPage( private class PriorityPage(
val page: ReaderPage, val page: ReaderPage,
val priority: Int, val priority: Int,
) : Comparable<PriorityPage> { ) : Comparable<PriorityPage> {
companion object { companion object {
private val idGenerator = AtomicInteger() private val idGenerator = AtomicInt(0)
} }
private val identifier = idGenerator.incrementAndGet() private val identifier = idGenerator.incrementAndFetch()
override fun compareTo(other: PriorityPage): Int { override fun compareTo(other: PriorityPage): Int {
val p = other.priority.compareTo(priority) val p = other.priority.compareTo(priority)
@@ -5,7 +5,7 @@ class InsertPage(val parent: ReaderPage) : ReaderPage(parent.index, parent.url,
override var chapter: ReaderChapter = parent.chapter override var chapter: ReaderChapter = parent.chapter
init { init {
status = State.READY status = State.Ready
stream = parent.stream stream = parent.stream
} }
} }
@@ -39,7 +39,7 @@ class ReaderPreferences(
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true) fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true) fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", false)
fun defaultReadingMode() = preferenceStore.getInt( fun defaultReadingMode() = preferenceStore.getInt(
"pref_default_reading_mode_key", "pref_default_reading_mode_key",
@@ -69,7 +69,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
private var config: Config? = null private var config: Config? = null
var onImageLoaded: (() -> Unit)? = null var onImageLoaded: (() -> Unit)? = null
var onImageLoadError: (() -> Unit)? = null var onImageLoadError: ((Throwable?) -> Unit)? = null
var onScaleChanged: ((newScale: Float) -> Unit)? = null var onScaleChanged: ((newScale: Float) -> Unit)? = null
var onViewClicked: (() -> Unit)? = null var onViewClicked: (() -> Unit)? = null
@@ -85,8 +85,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
@CallSuper @CallSuper
open fun onImageLoadError() { open fun onImageLoadError(error: Throwable?) {
onImageLoadError?.invoke() onImageLoadError?.invoke(error)
} }
@CallSuper @CallSuper
@@ -114,7 +114,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
onImageLoadError() onImageLoadError(e)
} }
}, },
) )
@@ -290,7 +290,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
this@ReaderPageImageView.onImageLoadError() this@ReaderPageImageView.onImageLoadError(e)
} }
}, },
) )
@@ -318,8 +318,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
setImage(ImageSource.bitmap(image.bitmap)) setImage(ImageSource.bitmap(image.bitmap))
isVisible = true isVisible = true
}, },
onError = { )
onImageLoadError() .listener(
onError = { _, result ->
onImageLoadError(result.throwable)
}, },
) )
.size(ViewSizeResolver(this@ReaderPageImageView)) .size(ViewSizeResolver(this@ReaderPageImageView))
@@ -395,8 +397,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
isVisible = true isVisible = true
this@ReaderPageImageView.onImageLoaded() this@ReaderPageImageView.onImageLoaded()
}, },
onError = { )
this@ReaderPageImageView.onImageLoadError() .listener(
onError = { _, result ->
onImageLoadError(result.throwable)
}, },
) )
.crossfade(false) .crossfade(false)
@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.InsertPage
@@ -22,12 +23,14 @@ import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import okio.Buffer import okio.Buffer
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import tachiyomi.i18n.MR
import kotlin.math.max import kotlin.math.max
/** /**
@@ -112,16 +115,16 @@ class PagerPageHolder(
} }
page.statusFlow.collectLatest { state -> page.statusFlow.collectLatest { state ->
when (state) { when (state) {
Page.State.QUEUE -> setQueued() Page.State.Queue -> setQueued()
Page.State.LOAD_PAGE -> setLoading() Page.State.LoadPage -> setLoading()
Page.State.DOWNLOAD_IMAGE -> { Page.State.DownloadImage -> {
setDownloading() setDownloading()
page.progressFlow.collectLatest { value -> page.progressFlow.collectLatest { value ->
progressIndicator?.setProgress(value) progressIndicator?.setProgress(value)
} }
} }
Page.State.READY -> setImage() Page.State.Ready -> setImage()
Page.State.ERROR -> setError() is Page.State.Error -> setError(state.error)
} }
} }
} }
@@ -213,7 +216,7 @@ class PagerPageHolder(
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { withUIContext {
setError() setError(e)
} }
} }
} }
@@ -405,9 +408,9 @@ class PagerPageHolder(
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
private fun setError() { private fun setError(error: Throwable?) {
progressIndicator?.hide() progressIndicator?.hide()
showErrorLayout() showErrorLayout(error)
} }
override fun onImageLoaded() { override fun onImageLoaded() {
@@ -418,9 +421,9 @@ class PagerPageHolder(
/** /**
* Called when an image fails to decode. * Called when an image fails to decode.
*/ */
override fun onImageLoadError() { override fun onImageLoadError(error: Throwable?) {
super.onImageLoadError() super.onImageLoadError(error)
setError() setError(error)
} }
/** /**
@@ -431,7 +434,7 @@ class PagerPageHolder(
viewer.activity.hideMenu() viewer.activity.hideMenu()
} }
private fun showErrorLayout(): ReaderErrorBinding { private fun showErrorLayout(error: Throwable?): ReaderErrorBinding {
if (errorLayout == null) { if (errorLayout == null) {
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true) errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
errorLayout?.actionRetry?.viewer = viewer errorLayout?.actionRetry?.viewer = viewer
@@ -446,12 +449,17 @@ class PagerPageHolder(
if (imageUrl.startsWith("http", true)) { if (imageUrl.startsWith("http", true)) {
errorLayout?.actionOpenInWebView?.viewer = viewer errorLayout?.actionOpenInWebView?.viewer = viewer
errorLayout?.actionOpenInWebView?.setOnClickListener { errorLayout?.actionOpenInWebView?.setOnClickListener {
val intent = WebViewActivity.newIntent(context, imageUrl) val sourceId = viewer.activity.viewModel.manga?.source
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
context.startActivity(intent) context.startActivity(intent)
} }
} }
} }
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
?: context.stringResource(MR.strings.decode_image_error)
errorLayout?.root?.isVisible = true errorLayout?.root?.isVisible = true
return errorLayout!! return errorLayout!!
} }
@@ -105,11 +105,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
pager.offscreenPageLimit = 1 pager.offscreenPageLimit = 1
pager.id = R.id.reader_pager pager.id = R.id.reader_pager
pager.adapter = adapter pager.adapter = adapter
pager.addOnPageChangeListener( pager.addOnPageChangeListener(pagerListener)
// SY -->
pagerListener,
// SY <--
)
pager.tapListener = { event -> pager.tapListener = { event ->
val viewPosition = IntArray(2) val viewPosition = IntArray(2)
pager.getLocationOnScreen(viewPosition) pager.getLocationOnScreen(viewPosition)
@@ -291,6 +287,9 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
* Sets the active [chapters] on this pager. * Sets the active [chapters] on this pager.
*/ */
private fun setChaptersInternal(chapters: ViewerChapters) { private fun setChaptersInternal(chapters: ViewerChapters) {
// Remove listener so the change in item doesn't trigger it
pager.removeOnPageChangeListener(pagerListener)
val forceTransition = val forceTransition =
config.alwaysShowChapterTransition || config.alwaysShowChapterTransition ||
adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition adapter.joinedItems.getOrNull(pager.currentItem)?.first is ChapterTransition
@@ -303,6 +302,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
moveToPage(pages[min(chapters.currChapter.requestedPage, pages.lastIndex)]) moveToPage(pages[min(chapters.currChapter.requestedPage, pages.lastIndex)])
pager.isVisible = true pager.isVisible = true
} }
pager.addOnPageChangeListener(pagerListener)
// Manually call onPageChange to update the UI
onPageChange(pager.currentItem)
} }
/** /**
@@ -473,13 +476,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
// SY --> // SY -->
fun setChaptersDoubleShift(chapters: ViewerChapters) { fun setChaptersDoubleShift(chapters: ViewerChapters) {
// Remove Listener since we're about to change the size of the items
// If we don't the size change could put us on a new chapter
pager.removeOnPageChangeListener(pagerListener)
setChaptersInternal(chapters) setChaptersInternal(chapters)
pager.addOnPageChangeListener(pagerListener)
// Since we removed the listener while shifting, call page change to update the ui
onPageChange(pager.currentItem)
} }
fun updateShifting(page: ReaderPage? = null) { fun updateShifting(page: ReaderPage? = null) {
@@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins import androidx.core.view.updateMargins
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.presentation.util.formattedMessage
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -25,11 +26,13 @@ import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import okio.Buffer import okio.Buffer
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
/** /**
* Holder of the webtoon reader for a single page of a chapter. * Holder of the webtoon reader for a single page of a chapter.
@@ -81,7 +84,7 @@ class WebtoonPageHolder(
refreshLayoutParams() refreshLayoutParams()
frame.onImageLoaded = { onImageDecoded() } frame.onImageLoaded = { onImageDecoded() }
frame.onImageLoadError = { setError() } frame.onImageLoadError = { error -> setError(error) }
frame.onScaleChanged = { viewer.activity.hideMenu() } frame.onScaleChanged = { viewer.activity.hideMenu() }
} }
@@ -136,16 +139,16 @@ class WebtoonPageHolder(
} }
page.statusFlow.collectLatest { state -> page.statusFlow.collectLatest { state ->
when (state) { when (state) {
Page.State.QUEUE -> setQueued() Page.State.Queue -> setQueued()
Page.State.LOAD_PAGE -> setLoading() Page.State.LoadPage -> setLoading()
Page.State.DOWNLOAD_IMAGE -> { Page.State.DownloadImage -> {
setDownloading() setDownloading()
page.progressFlow.collectLatest { value -> page.progressFlow.collectLatest { value ->
progressIndicator.setProgress(value) progressIndicator.setProgress(value)
} }
} }
Page.State.READY -> setImage() Page.State.Ready -> setImage()
Page.State.ERROR -> setError() is Page.State.Error -> setError(state.error)
} }
} }
} }
@@ -209,7 +212,7 @@ class WebtoonPageHolder(
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { withUIContext {
setError() setError(e)
} }
} }
} }
@@ -243,9 +246,9 @@ class WebtoonPageHolder(
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
private fun setError() { private fun setError(error: Throwable?) {
progressContainer.isVisible = false progressContainer.isVisible = false
initErrorLayout() initErrorLayout(error)
} }
/** /**
@@ -275,7 +278,7 @@ class WebtoonPageHolder(
/** /**
* Initializes a button to retry pages. * Initializes a button to retry pages.
*/ */
private fun initErrorLayout(): ReaderErrorBinding { private fun initErrorLayout(error: Throwable?): ReaderErrorBinding {
if (errorLayout == null) { if (errorLayout == null) {
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true) errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt()) errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
@@ -289,12 +292,17 @@ class WebtoonPageHolder(
if (imageUrl != null) { if (imageUrl != null) {
if (imageUrl.startsWith("http", true)) { if (imageUrl.startsWith("http", true)) {
errorLayout?.actionOpenInWebView?.setOnClickListener { errorLayout?.actionOpenInWebView?.setOnClickListener {
val intent = WebViewActivity.newIntent(context, imageUrl) val sourceId = viewer.activity.viewModel.manga?.source
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
context.startActivity(intent) context.startActivity(intent)
} }
} }
} }
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
?: context.stringResource(MR.strings.decode_image_error)
return errorLayout!! return errorLayout!!
} }
@@ -40,19 +40,19 @@ class SettingsScreen(
Destination.Tracking.id -> SettingsTrackingScreen Destination.Tracking.id -> SettingsTrackingScreen
else -> SettingsMainScreen else -> SettingsMainScreen
}, },
content = { onBackPressed = null,
val pop: () -> Unit = { ) {
if (it.canPop) { val pop: () -> Unit = {
it.pop() if (it.canPop) {
} else { it.pop()
parentNavigator.pop() } else {
} parentNavigator.pop()
} }
CompositionLocalProvider(LocalBackPress provides pop) { }
DefaultNavigatorScreenTransition(navigator = it) CompositionLocalProvider(LocalBackPress provides pop) {
} DefaultNavigatorScreenTransition(navigator = it)
}, }
) }
} else { } else {
Navigator( Navigator(
screen = when (destination) { screen = when (destination) {
@@ -61,6 +61,7 @@ class SettingsScreen(
Destination.Tracking.id -> SettingsTrackingScreen Destination.Tracking.id -> SettingsTrackingScreen
else -> SettingsAppearanceScreen else -> SettingsAppearanceScreen
}, },
onBackPressed = null,
) { ) {
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal) val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
TwoPanelBox( TwoPanelBox(
@@ -4,9 +4,11 @@ package eu.kanade.tachiyomi.util.view
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.RoundedCorner
import android.view.View import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -95,3 +97,22 @@ fun View?.isVisibleOnScreen(): Boolean {
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels) Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
return actualPosition.intersect(screen) return actualPosition.intersect(screen)
} }
/**
* Returns window radius (in pixel) applied to this view
*/
fun View.getWindowRadius(): Int {
val rad = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val windowInsets = rootWindowInsets
listOfNotNull(
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT),
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT),
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT),
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT),
)
.minOfOrNull { it.radius }
} else {
null
}
return rad ?: 0
}
+1 -1
View File
@@ -136,7 +136,7 @@ class GalleryAdder(
// Use manga in DB if possible, otherwise, make a new manga // Use manga in DB if possible, otherwise, make a new manga
var manga = getManga.await(cleanedMangaUrl, source.id) var manga = getManga.await(cleanedMangaUrl, source.id)
?: networkToLocalManga.await( ?: networkToLocalManga(
Manga.create().copy( Manga.create().copy(
source = source.id, source = source.id,
url = cleanedMangaUrl, url = cleanedMangaUrl,
@@ -6,8 +6,6 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -201,13 +199,7 @@ class SettingsDebugScreen : Screen() {
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(color = Color.White.copy(alpha = 0.3F)) .background(color = Color.White.copy(alpha = 0.3F))
.pointerInput(running && result == null) { .pointerInput(running && result == null) {},
forEachGesture {
awaitPointerEventScope {
waitForUpOrCancellation()?.consume()
}
}
},
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CircularProgressIndicator() CircularProgressIndicator()
@@ -1,6 +1,5 @@
package exh.favorites package exh.favorites
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
@@ -11,6 +10,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import mihon.domain.manga.model.toDomainManga
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries import tachiyomi.domain.manga.interactor.DeleteFavoriteEntries
@@ -2,12 +2,12 @@ package exh.md.follows
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import tachiyomi.data.source.SourcePagingSource import tachiyomi.data.source.BaseSourcePagingSource
/** /**
* LatestUpdatesPager inherited from the general Pager. * LatestUpdatesPager inherited from the general Pager.
*/ */
class MangaDexFollowsPagingSource(val mangadex: MangaDex) : SourcePagingSource(mangadex) { class MangaDexFollowsPagingSource(val mangadex: MangaDex) : BaseSourcePagingSource(mangadex) {
override suspend fun requestNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return mangadex.fetchFollows(currentPage) return mangadex.fetchFollows(currentPage)
@@ -84,7 +84,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
val duplicateManga = screenModel.getDuplicateLibraryManga(manga) val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
when { when {
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga)) manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
duplicateManga != null -> screenModel.setDialog( duplicateManga.isNotEmpty() -> screenModel.setDialog(
BrowseSourceScreenModel.Dialog.AddDuplicateManga( BrowseSourceScreenModel.Dialog.AddDuplicateManga(
manga, manga,
duplicateManga, duplicateManga,
@@ -103,14 +103,15 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
is BrowseSourceScreenModel.Dialog.Migrate -> {} is BrowseSourceScreenModel.Dialog.Migrate -> {}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> { is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog( DuplicateMangaDialog(
duplicates = dialog.duplicates,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onConfirm = { screenModel.addFavorite(dialog.manga) }, onConfirm = { screenModel.addFavorite(dialog.manga) },
onOpenManga = { navigator.push(MangaScreen(dialog.duplicate.id)) }, onOpenManga = { navigator.push(MangaScreen(it.id)) },
onMigrate = { onMigrate = {
PreMigrationScreen.navigateToMigration( PreMigrationScreen.navigateToMigration(
Injekt.get<UnsortedPreferences>().skipPreMigration().get(), Injekt.get<UnsortedPreferences>().skipPreMigration().get(),
navigator, navigator,
dialog.duplicate.id, it.id,
dialog.manga.id, dialog.manga.id,
) )
}, },
@@ -8,12 +8,12 @@ import exh.source.getMainSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.data.source.BaseSourcePagingSource
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.repository.SourcePagingSourceType
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) { class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
override fun createSourcePagingSource(query: String, filters: FilterList): SourcePagingSourceType { override fun createSourcePagingSource(query: String, filters: FilterList): BaseSourcePagingSource {
return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex) return MangaDexFollowsPagingSource(source.getMainSource() as MangaDex)
} }
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import exh.recs.sources.RecommendationPagingSource import exh.recs.sources.RecommendationPagingSource
@@ -23,6 +22,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mihon.domain.manga.model.toDomainManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@@ -85,7 +85,7 @@ open class RecommendsScreenModel(
val recSourceId = recSource.associatedSourceId val recSourceId = recSource.associatedSourceId
if (recSourceId != null) { if (recSourceId != null) {
// If the recommendation is associated with a source, resolve it // If the recommendation is associated with a source, resolve it
networkToLocalManga.await(it.toDomainManga(recSourceId)) networkToLocalManga(it.toDomainManga(recSourceId))
} else { } else {
// Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch // Otherwise, skip this step. The user will be prompted to choose a source via SmartSearch
it.toDomainManga(-1) it.toDomainManga(-1)
@@ -5,7 +5,6 @@ import android.net.wifi.WifiManager
import android.os.PowerManager import android.os.PowerManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.net.toUri import androidx.core.net.toUri
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -26,6 +25,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.domain.manga.model.toDomainManga
import tachiyomi.data.source.NoResultsException import tachiyomi.data.source.NoResultsException
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
@@ -201,8 +201,7 @@ class RecommendationSearchHelper(val context: Context) {
return filterNot { manga -> return filterNot { manga ->
// Source recommendations can be directly resolved, if the recommendation is from the same source // Source recommendations can be directly resolved, if the recommendation is from the same source
recSource.associatedSourceId?.let { srcId -> recSource.associatedSourceId?.let { srcId ->
return@filterNot networkToLocalManga return@filterNot networkToLocalManga(manga.toDomainManga(srcId))
.await(manga.toDomainManga(srcId))
.let { local -> libraryManga.any { it.id == local.id } } .let { local -> libraryManga.any { it.id == local.id } }
} }
@@ -14,8 +14,8 @@ import exh.source.isMdBasedSource
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.source.BaseSourcePagingSource
import tachiyomi.data.source.NoResultsException import tachiyomi.data.source.NoResultsException
import tachiyomi.data.source.SourcePagingSource
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
@@ -29,7 +29,7 @@ import uy.kohesive.injekt.injectLazy
abstract class RecommendationPagingSource( abstract class RecommendationPagingSource(
protected val manga: Manga, protected val manga: Manga,
source: CatalogueSource? = null, source: CatalogueSource? = null,
) : SourcePagingSource(source) { ) : BaseSourcePagingSource(source) {
// Display name // Display name
abstract val name: String abstract val name: String
@@ -1,9 +1,9 @@
package exh.smartsearch package exh.smartsearch
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import mihon.domain.manga.model.toDomainManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
class SmartSourceSearchEngine( class SmartSourceSearchEngine(
@@ -28,7 +28,7 @@ class SmartSearchScreenModel(
val result = try { val result = try {
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle) val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
if (resultManga != null) { if (resultManga != null) {
val localManga = networkToLocalManga.await(resultManga) val localManga = networkToLocalManga(resultManga)
SearchResults.Found(localManga) SearchResults.Found(localManga)
} else { } else {
SearchResults.NotFound SearchResults.NotFound
@@ -43,7 +43,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded" app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra,mig_delete_downloaded,mig_notes"
app:flow_horizontalBias="0" app:flow_horizontalBias="0"
app:flow_horizontalGap="8dp" app:flow_horizontalGap="8dp"
app:flow_horizontalStyle="packed" app:flow_horizontalStyle="packed"
@@ -94,6 +94,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:checked="true"
android:text="@string/delete_downloaded" /> android:text="@string/delete_downloaded" />
<CheckBox
android:id="@+id/mig_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/action_notes" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<View <View
+1
View File
@@ -7,6 +7,7 @@
android:gravity="center"> android:gravity="center">
<TextView <TextView
android:id="@+id/error_message"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
@@ -1,6 +1,7 @@
package mihon.buildlogic package mihon.buildlogic
import org.gradle.api.Project import org.gradle.api.Project
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -19,9 +20,18 @@ fun Project.getGitSha(): String {
private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") private val BUILD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
@Suppress("UnusedReceiverParameter") /**
fun Project.getBuildTime(): String { * @param useLastCommitTime If `true`, the build time is based on the timestamp of the last Git commit;
return LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER) * otherwise, the current time is used. Both are in UTC.
* @return A formatted string representing the build time. The format used is defined by [BUILD_TIME_FORMATTER].
*/
fun Project.getBuildTime(useLastCommitTime: Boolean): String {
return if (useLastCommitTime) {
val epoch = runCommand("git log -1 --format=%ct").toLong()
Instant.ofEpochSecond(epoch).atOffset(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
} else {
LocalDateTime.now(ZoneOffset.UTC).format(BUILD_TIME_FORMATTER)
}
} }
private fun Project.runCommand(command: String): String { private fun Project.runCommand(command: String): String {
+1 -1
View File
@@ -57,7 +57,7 @@ dependencies {
// SY --> // SY -->
implementation(sylibs.xlog) implementation(sylibs.xlog)
implementation(libs.injekt.core) implementation(libs.injekt)
implementation(sylibs.exifinterface) implementation(sylibs.exifinterface)
// SY <-- // SY <--
} }
@@ -19,7 +19,7 @@ class NetworkPreferences(
fun defaultUserAgent(): Preference<String> { fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString( return preferenceStore.getString(
"default_user_agent", "default_user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36",
) )
} }
} }
@@ -16,20 +16,23 @@ import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
val jsonMime = "application/json; charset=utf-8".toMediaType() val jsonMime = "application/json; charset=utf-8".toMediaType()
@OptIn(ExperimentalAtomicApi::class)
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber. // Since Call is a one-shot type, clone it for each new subscriber.
val call = clone() val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure. // Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription { val requestArbiter = object : Producer, Subscription {
val boolean = AtomicBoolean(false)
override fun request(n: Long) { override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
try { try {
val response = call.execute() val response = call.execute()
@@ -34,13 +34,14 @@ private val mapper = { cursor: SqlCursor ->
favorite_modified_at = cursor.getLong(22), favorite_modified_at = cursor.getLong(22),
version = cursor.getLong(23)!!, version = cursor.getLong(23)!!,
is_syncing = cursor.getLong(24)!!, is_syncing = cursor.getLong(24)!!,
totalCount = cursor.getLong(25)!!, notes = cursor.getString(25)!!,
readCount = cursor.getDouble(26)!!, totalCount = cursor.getLong(26)!!,
latestUpload = cursor.getLong(27)!!, readCount = cursor.getDouble(27)!!,
chapterFetchedAt = cursor.getLong(28)!!, latestUpload = cursor.getLong(28)!!,
lastRead = cursor.getLong(29)!!, chapterFetchedAt = cursor.getLong(29)!!,
bookmarkCount = cursor.getDouble(30)!!, lastRead = cursor.getLong(30)!!,
category = cursor.getLong(31)!!, bookmarkCount = cursor.getDouble(31)!!,
category = cursor.getLong(32)!!,
) )
} }
@@ -7,7 +7,10 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
@@ -127,6 +130,7 @@ private suspend fun CoroutineDispatcher.acquireTransactionThread(
/** /**
* A [CoroutineContext.Element] that indicates there is an on-going database transaction. * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
*/ */
@OptIn(ExperimentalAtomicApi::class)
private class TransactionElement( private class TransactionElement(
private val transactionThreadControlJob: Job, private val transactionThreadControlJob: Job,
val transactionDispatcher: ContinuationInterceptor, val transactionDispatcher: ContinuationInterceptor,
@@ -143,14 +147,14 @@ private class TransactionElement(
* when [release] is invoked then the transaction job is cancelled and the transaction thread * when [release] is invoked then the transaction job is cancelled and the transaction thread
* is released. * is released.
*/ */
private val referenceCount = AtomicInteger(0) private val referenceCount = AtomicInt(0)
fun acquire() { fun acquire() {
referenceCount.incrementAndGet() referenceCount.incrementAndFetch()
} }
fun release() { fun release() {
val count = referenceCount.decrementAndGet() val count = referenceCount.decrementAndFetch()
if (count < 0) { if (count < 0) {
throw IllegalStateException("Transaction was never started or was already released") throw IllegalStateException("Transaction was never started or was already released")
} else if (count == 0) { } else if (count == 0) {
@@ -3,6 +3,7 @@ package tachiyomi.data.manga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.view.LibraryView import tachiyomi.view.LibraryView
object MangaMapper { object MangaMapper {
@@ -36,6 +37,7 @@ object MangaMapper {
version: Long, version: Long,
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
isSyncing: Long, isSyncing: Long,
notes: String,
): Manga = Manga( ): Manga = Manga(
id = id, id = id,
source = source, source = source,
@@ -62,6 +64,7 @@ object MangaMapper {
lastModifiedAt = lastModifiedAt, lastModifiedAt = lastModifiedAt,
favoriteModifiedAt = favoriteModifiedAt, favoriteModifiedAt = favoriteModifiedAt,
version = version, version = version,
notes = notes,
) )
fun mapLibraryManga( fun mapLibraryManga(
@@ -93,6 +96,7 @@ object MangaMapper {
favoriteModifiedAt: Long?, favoriteModifiedAt: Long?,
version: Long, version: Long,
isSyncing: Long, isSyncing: Long,
notes: String,
totalCount: Long, totalCount: Long,
readCount: Double, readCount: Double,
latestUpload: Long, latestUpload: Long,
@@ -129,6 +133,7 @@ object MangaMapper {
favoriteModifiedAt, favoriteModifiedAt,
version, version,
isSyncing, isSyncing,
notes,
), ),
category = category, category = category,
totalChapters = totalCount, totalChapters = totalCount,
@@ -139,6 +144,71 @@ object MangaMapper {
lastRead = lastRead, lastRead = lastRead,
) )
fun mapMangaWithChapterCount(
id: Long,
source: Long,
url: String,
artist: String?,
author: String?,
description: String?,
genre: List<String>?,
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
// SY -->
@Suppress("UNUSED_PARAMETER")
filteredScanlators: String?,
// SY <--
updateStrategy: UpdateStrategy,
calculateInterval: Long,
lastModifiedAt: Long,
favoriteModifiedAt: Long?,
version: Long,
isSyncing: Long,
notes: String,
totalCount: Long,
): MangaWithChapterCount = MangaWithChapterCount(
manga = mapManga(
id,
source,
url,
artist,
author,
description,
genre,
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
// SY -->
null,
// SY <--
updateStrategy,
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
version,
isSyncing,
notes,
),
chapterCount = totalCount,
)
fun mapLibraryView(libraryView: LibraryView): LibraryManga { fun mapLibraryView(libraryView: LibraryView): LibraryManga {
return LibraryManga( return LibraryManga(
manga = Manga( manga = Manga(
@@ -165,6 +235,7 @@ object MangaMapper {
lastModifiedAt = libraryView.last_modified_at, lastModifiedAt = libraryView.last_modified_at,
favoriteModifiedAt = libraryView.favorite_modified_at, favoriteModifiedAt = libraryView.favorite_modified_at,
version = libraryView.version, version = libraryView.version,
notes = libraryView.notes,
), ),
category = libraryView.category, category = libraryView.category,
totalChapters = libraryView.totalCount, totalChapters = libraryView.totalCount,
@@ -11,6 +11,7 @@ import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MangaWithChapterCount
import tachiyomi.domain.manga.repository.MangaRepository import tachiyomi.domain.manga.repository.MangaRepository
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
@@ -73,9 +74,9 @@ class MangaRepositoryImpl(
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) } return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, MangaMapper::mapManga) }
} }
override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga> { override suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount> {
return handler.awaitList { return handler.awaitList {
mangasQueries.getDuplicateLibraryManga(title, id, MangaMapper::mapManga) mangasQueries.getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount)
} }
} }
@@ -105,39 +106,6 @@ class MangaRepositoryImpl(
} }
} }
override suspend fun insert(manga: Manga): Long? {
return handler.awaitOneOrNullExecutable(inTransaction = true) {
// SY -->
if (mangasQueries.getIdByUrlAndSource(manga.url, manga.source).executeAsOneOrNull() != null) {
return@awaitOneOrNullExecutable mangasQueries.getIdByUrlAndSource(manga.url, manga.source)
}
// SY <--
mangasQueries.insert(
source = manga.source,
url = manga.url,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre,
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = manga.nextUpdate,
calculateInterval = manga.fetchInterval.toLong(),
initialized = manga.initialized,
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
version = manga.version,
)
mangasQueries.selectLastInsertedRowId()
}
}
override suspend fun update(update: MangaUpdate): Boolean { override suspend fun update(update: MangaUpdate): Boolean {
return try { return try {
partialUpdate(update) partialUpdate(update)
@@ -158,6 +126,44 @@ class MangaRepositoryImpl(
} }
} }
override suspend fun insertNetworkManga(manga: List<Manga>): List<Manga> {
return handler.await(inTransaction = true) {
manga.map {
mangasQueries.insertNetworkManga(
source = it.source,
url = it.url,
// SY -->
artist = it.ogArtist,
author = it.ogAuthor,
description = it.ogDescription,
genre = it.ogGenre,
title = it.ogTitle,
status = it.ogStatus,
thumbnailUrl = it.ogThumbnailUrl,
// SY <--
favorite = it.favorite,
lastUpdate = it.lastUpdate,
nextUpdate = it.nextUpdate,
calculateInterval = it.fetchInterval.toLong(),
initialized = it.initialized,
viewerFlags = it.viewerFlags,
chapterFlags = it.chapterFlags,
coverLastModified = it.coverLastModified,
dateAdded = it.dateAdded,
updateStrategy = it.updateStrategy,
version = it.version,
// SY -->
updateTitle = it.ogTitle.isNotBlank(),
updateCover = !it.ogThumbnailUrl.isNullOrBlank(),
// SY <--
updateDetails = it.initialized,
mapper = MangaMapper::mapManga,
)
.executeAsOne()
}
}
}
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) { private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
handler.await(inTransaction = true) { handler.await(inTransaction = true) {
mangaUpdates.forEach { value -> mangaUpdates.forEach { value ->
@@ -184,6 +190,7 @@ class MangaRepositoryImpl(
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
version = value.version, version = value.version,
isSyncing = 0, isSyncing = 0,
notes = value.notes,
) )
} }
} }

Some files were not shown because too many files have changed in this diff Show More