Compare commits

...

183 commits

Author SHA1 Message Date
17879ddc5a
chore: Bump version code
Some checks failed
Build app / Build app (push) Has been cancelled
Mirror Repository / mirror (push) Has been cancelled
2025-06-02 13:52:46 +07:00
7f83c117be
chore: Sync project [skip ci] 2025-06-02 10:04:20 +07:00
abbe606473
revert: "refactor: Replace Requery's SQLite with AndroidX's new KMP SQLite"
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
This reverts commit f604e4e256.
2025-06-01 18:05:35 +07:00
7ac42d5545
docs: Sync changelog 2025-06-01 16:44:45 +07:00
renovate[bot]
f90e2a1425
fix(deps): Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1 (#440)
* fix(deps): Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1

* fix: Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
2025-06-01 16:43:59 +07:00
f604e4e256
refactor: Replace Requery's SQLite with AndroidX's new KMP SQLite
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
2025-06-01 14:55:19 +07:00
a04ea9f5ea
chore(deps): Also grab dependency from sonatype just in case
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
2025-05-31 11:11:21 +07:00
43d4d5404d
docs: Sync changelog 2025-05-31 07:36:21 +07:00
renovate[bot]
54df9436b8
fix(deps): Update serialization to v1.8.1 (#446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:33:22 +07:00
renovate[bot]
cd5cdbe746
fix(deps): Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:32:55 +07:00
renovate[bot]
41662979fe
fix(deps): Update lifecycle to v2.9.0 (#445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:26:24 +07:00
renovate[bot]
f68e9df74d
fix(deps): Update dependency org.jsoup:jsoup to v1.20.1 (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:26:05 +07:00
renovate[bot]
a77d315922
fix(deps): Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0 (#442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:25:49 +07:00
renovate[bot]
4e2c4aef8a
fix(deps): Update dependency io.mockk:mockk to v1.14.2 (#441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:25:31 +07:00
renovate[bot]
d80b53ba78
fix(deps): Update dependency io.coil-kt.coil3:coil-bom to v3.2.0 (#438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:25:16 +07:00
renovate[bot]
a1f6eb6524
fix(deps): Update dependency com.squareup.okio:okio to v3.12.0 (#437)
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:24:58 +07:00
renovate[bot]
9f256bb8c6
fix(deps): Update dependency com.google.firebase:firebase-bom to v33.14.0 (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:24:40 +07:00
renovate[bot]
1d49d65961
fix(deps): Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0 (#435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:24:24 +07:00
renovate[bot]
9ccdd36c46
fix(deps): Update dependency com.github.requery:sqlite-android to v3.49.0 (#434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:24:04 +07:00
renovate[bot]
48938f02dd
fix(deps): Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0 (#432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:21:18 +07:00
renovate[bot]
63435b933a
fix(deps): Update dependency androidx.window:window to v1.4.0 (#431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:20:59 +07:00
renovate[bot]
ef49bf3321
fix(deps): Update dependency androidx.webkit:webkit to v1.13.0 (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:20:44 +07:00
renovate[bot]
c05ba1a8fb
fix(deps): Update dependency androidx.sqlite:sqlite-ktx to v2.5.1 (#429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:16:58 +07:00
renovate[bot]
764d52a729
fix(deps): Update dependency androidx.sqlite:sqlite to v2.5.1 (#428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:16:45 +07:00
renovate[bot]
af2be0d2d0
fix(deps): Update dependency androidx.recyclerview:recyclerview to v1.4.0 (#427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:14:07 +07:00
renovate[bot]
733fcbba4a
fix(deps): Update dependency androidx.core:core-ktx to v1.16.0 (#426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:13:50 +07:00
renovate[bot]
b6e1cabc59
fix(deps): Update dependency androidx.compose:compose-bom to v2025.05.01 (#425)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:13:35 +07:00
renovate[bot]
3867aabff1
fix(deps): Update aboutlibraries to v11.6.3 (#424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:13:20 +07:00
renovate[bot]
ca6bb95b84
chore(deps): Update plugin kotlinter to v5.1.0 (#423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:12:59 +07:00
renovate[bot]
4b7564e410
chore(deps): Update plugin gradle-versions to v0.52.0 (#422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:11:36 +07:00
renovate[bot]
8d3cfffa66
fix(deps): Update okhttp monorepo to v5.0.0-alpha.16 (#421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com>
2025-05-31 07:11:10 +07:00
renovate[bot]
97339689c6
fix(deps): Update moko to v0.24.5 (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:10:01 +07:00
renovate[bot]
9453c3e808
fix(deps): Update kotlin monorepo to v2.1.21 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:09:41 +07:00
renovate[bot]
6f03935c17
fix(deps): Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:09:25 +07:00
renovate[bot]
f1597bd95c
fix(deps): Update dependency me.zhanghai.android.libarchive:library to v1.1.5 (#417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:09:09 +07:00
renovate[bot]
903a37e390
fix(deps): Update dependency io.insert-koin:koin-bom to v4.0.4 (#416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:08:54 +07:00
renovate[bot]
1655540a16
fix(deps): Update dependency com.android.tools:desugar_jdk_libs to v2.1.5 (#415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:08:40 +07:00
renovate[bot]
6a7b386127
fix(deps): Update dependency androidx.work:work-runtime-ktx to v2.10.1 (#414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:08:23 +07:00
renovate[bot]
75191dde05
fix(deps): Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:08:04 +07:00
renovate[bot]
db0af71901
chore(deps): Update plugin firebase-crashlytics to v3.0.3 (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-31 07:07:50 +07:00
renovate[bot]
89c5e997cc
chore(deps): Update null2264/actions digest to 363cb9c (#411)
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
* chore(deps): Update null2264/actions digest to 363cb9c

* fix: Action was renamed

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com>
2025-05-30 08:49:47 +07:00
e22559b2df
fix(renovate): Ignore more jitpack packages [skip ci] 2025-05-30 08:33:35 +07:00
AwkwardPeak7
370bb62ef9
refactor: Change Page.State to sealed interface
Some checks failed
Build app / Build app (push) Has been cancelled
Mirror Repository / mirror (push) Has been cancelled
2025-05-28 09:19:42 +07:00
AntsyLich
18528fbd92
fix: Fix mark existing duplicate read chapters as read option not working in some cases 2025-05-28 09:05:14 +07:00
7964ac87c6
fix: Wrong pref to bound
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
I should really stop coding in the middle of the night
2025-05-27 22:25:19 +07:00
850151720b
feat: Mark duplicate read chapters as read
Some checks are pending
Build app / Build app (push) Waiting to run
Mirror Repository / mirror (push) Waiting to run
This also refactor how chapters progress are saved. Chapters' progress now save when user "flipped" the page.

Closes GH-409

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-05-27 21:47:41 +07:00
d3050d5799
fix: Fix weblate [skip ci] 2025-05-26 08:15:39 +07:00
c7d2ff0970
chore: Ignore weblate config [skip ci] 2025-05-26 08:00:32 +07:00
524c00fd44
docs(README): Mirror the project to my personal git hosting [skip ci]
Just in case
2025-05-26 07:40:38 +07:00
93f819c236
docs(README): Mirror the project to my personal git hosting [skip ci]
Just in case
2025-05-26 07:37:00 +07:00
1c73b925b1
chore(github): Everyone gets to vote [skip ci] 2025-05-23 08:07:50 +07:00
Weblate (bot)
ea179979b1
chore(i18n): Translations update from Hosted Weblate (#390)
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/es/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fi/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fil/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fr/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/hi/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hans/
Translation: Yōkai/Yōkai

Co-authored-by: Hiirbaf <thefgc8@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: MARTINAT Noah <noahmartinat@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Thibault <tib2935@gmail.com>
Co-authored-by: UnTamed Fury <prince.16.tamoli@gmail.com>
Co-authored-by: zhongfly <11155705+zhongfly@users.noreply.github.com>
2025-05-14 10:51:39 +07:00
71c26b77fc
chore: Sync project [skip ci] 2025-05-14 04:37:33 +07:00
43a5e8edd8
fix: Disable MAL's custom user-agent 2025-05-14 04:13:41 +07:00
Hiirbaf
33d7c3cd2b
feat: update user agent (#401)
* Switch default user agent to Android Chrome

* Emparejamiento con mihon

* Update User-Agent
2025-05-14 03:44:48 +07:00
0e0e865cb9
docs(CHANGELOG): Add missing credit [skip ci] 2025-05-11 19:54:26 +07:00
AwkwardPeak7
2b2e0491e8
fix: staggered grid cover being squashed for local source (#398)
* fix: cover ratio NaN for local source

* unused

* Update CHANGELOG.md
2025-05-11 19:52:43 +07:00
f035454150
chore: Sync project [skip ci] 2025-05-09 11:18:55 +07:00
96f88d5e90
docs: Sync changelog [skip ci] 2025-05-09 11:17:58 +07:00
f362b0bda0
fix: Fix saved search not restoring Filter.Group properly
This happened when Filter.Group ordering doesn't match the old ordering.
2025-05-09 10:44:30 +07:00
f74662c0f3
docs: Add missing credits [skip ci] 2025-05-05 07:01:51 +07:00
65639391b7
docs: Sync changelog 2025-05-05 06:49:29 +07:00
Hiirbaf
94c314559b
feat: Enable/Disable Sources Swipe (#396)
* Update SourceItem.kt

* Update SettingsBrowseController.kt 1

* Update SettingsBrowseController.kt

* Update SettingsBrowseController.kt

* Update strings.xml

* Update SettingsBrowseController.kt

* Update UiPreferences.kt

* Update SettingsBrowseController.kt

* Update SourceItem.kt
2025-05-05 06:46:29 +07:00
66241774dc chore(deps): Update dependency gradle to v8.12 2025-05-05 06:05:45 +07:00
ea634a5ce3
chore: Bump version to v1.10.0 [skip ci]
Changing the version scheme to:
- Major -> Basically a rewrite
- Minor -> Feature releases / big changes
- Macro -> Bug fixes
2025-04-18 12:07:46 +07:00
271e440014
fix: Prevent potential "Comparison method violates its general contract!" crash 2025-04-17 17:12:31 +07:00
Lee Shuen Fei
8be33e0f81
feat: Display the number of filtered manga in each category's
header when searching in library (#387)
2025-04-15 07:05:15 +07:00
f13f98f19a
feat: Add the ability to save search queries
I got tired of putting the same tag over and over, so...
2025-04-14 21:02:12 +07:00
7a08ca294a
fix: Fix source filter buttom sheet unable to be fully scrolled to the bottom
This bug has been annoying me for a long time, classic RecyclerView moment
2025-04-13 18:21:30 +07:00
4faa641739
docs: Sync changelog [skip ci] 2025-04-12 07:18:19 +07:00
Weblate (bot)
ebd891fa75
chore(i18n): Translations update from Hosted Weblate (#330)
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai-plurals/fi/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/es/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fi/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fil/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fr/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/id/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/ja/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/ru/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/tr/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hans/
Translation: Yōkai/Yōkai
Translation: Yōkai/Yōkai Plurals

Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
Co-authored-by: Alexander Sergeev <hiyajo.maho@rambler.ru>
Co-authored-by: Hiirbaf <thefgc8@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: MARTINAT Noah <noahmartinat@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Tachibana Saza <tachibanasaza@proton.me>
Co-authored-by: zhongfly <11155705+zhongfly@users.noreply.github.com>
2025-04-12 07:17:22 +07:00
Hiirbaf
d46f5fb73e
fix: Temporarily disable file log (#380)
Temporary solution to the problem of stopping working in the background
2025-03-29 09:52:00 +07:00
AntsyLich
d6c5a9a7c2
chore: Tweak .editorconfig [skip ci] 2025-03-10 06:53:32 +07:00
MajorTanya
2208a81013
fix: Add Infinix system app to list of invalid browsers
`com.transsion.resolver` being picked by the system as a suitable
browser caused a Mihon user with an Infinix device to be unable to
open any links in browsers, including tracker login and opening a
WebView page in a real browser.
2025-02-05 07:54:26 +07:00
0bf55a8ca0
ci: Tweak GitHub Actions and switch from Adopt to Temurin JDK 2025-02-02 07:02:23 +07:00
4dd8aece0c
fix: Temporarily hide the experimental compose library switch for nightly 2025-01-24 18:54:06 +07:00
ece849b008
fix(AppBar): Use maxLayoutHeight instead of constraints.maxHeight
Also revert padding changes
2025-01-17 10:16:57 +07:00
d2ddf7dfb0
fix(AppBar): Also adjust the other placeables' padding 2025-01-17 09:35:32 +07:00
8b53e5ad10
fix(AppBar): Adjust title padding 2025-01-17 09:26:22 +07:00
63cdf247b4
chore: Sync AppBar code with upstream 2025-01-17 09:14:35 +07:00
9ed12ef07c
fix: Forgor to put ! 2025-01-16 17:50:21 +07:00
86b01a297f
fix: Can't set stable id like that 2025-01-14 03:59:47 +07:00
d6ffbe15ee
fix: Title is only lateinit on SMangaImpl 2025-01-13 13:46:29 +07:00
915ce20904
fix: Fix build 2025-01-13 13:30:11 +07:00
9e5d13f261
fix: Fix lateinit error 2025-01-13 13:22:19 +07:00
33fa77d527
fix: Fix some NPE crashes 2025-01-13 12:39:18 +07:00
baaa841278
fix: Explicitly disable stable ids 2025-01-13 12:23:18 +07:00
453ea32bc9
chore: Hide compose library on prod build 2025-01-13 11:33:23 +07:00
f37e657a9b
fix: Prevent lateinit crash 2025-01-13 11:29:07 +07:00
258708b038
fix: Fix build 2025-01-10 11:10:28 +07:00
c6da3325b3
fix(AppBar): Wrap scroll behaviour with remember { } 2025-01-10 11:06:11 +07:00
c9a90f6847
chore(library/compose): View<->Compose Interop
REF: https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#parent-view-child-compose
2025-01-09 07:22:21 +07:00
9cf1fbb118
fix(i18n): Use behavior instead of behaviour
The base should use US English not British English... I'm too used to
British English :^)
2025-01-08 07:55:00 +07:00
f01ace94be
chore(library/compose): LibraryItem data classes 2025-01-08 05:54:27 +07:00
ad22250265
fix(library/compose): Don't left the title empty 2025-01-07 19:05:51 +07:00
48c2ad9b33
refactor(library/compose): StateCoroutinePresenter 2025-01-07 18:59:08 +07:00
7d9c0faf86
fix: Compose library is not rendering anything 2025-01-07 10:08:40 +07:00
6614bd3ed8
docs(template): Wrong name [skip ci] 2025-01-07 08:23:02 +07:00
c6c40ffb71
docs(template): Replace broken link and instruct people how to find
extension repo maintainer [skip ci]
2025-01-07 08:20:57 +07:00
d0d322fd67
refactor(extension/repo): Use ScreenModel instead of ViewModel 2025-01-07 07:56:43 +07:00
d655c3e09a
chore: WIP compose library page 2025-01-07 07:36:00 +07:00
6a680facd5
refactor(extension): Installer abstraction 2025-01-07 05:45:50 +07:00
0565fc2665
fix: Selected icon for Random should be a reload icon 2025-01-06 08:02:00 +07:00
568859891a
fix: Forgor about the bubble 2025-01-05 19:27:29 +07:00
7fc95e3029
feat: Random sort 2025-01-05 19:19:28 +07:00
eebc3dc822
docs: Sync changelog 2025-01-05 18:20:08 +07:00
968639a59b
chore: Fix typo 2025-01-05 18:16:21 +07:00
Ahmad Ansori Palembani
cae0332ef9
refactor(library): Store sectioned library instead of flatten version of it (#336)
* refactor(library): Store sectioned first before flattening it out

* fix: Fix build, also rename some variables and move stuff around

* chore: Replace findCurrentCategory with currentCategory getter

* fix: Empty category can't be collapsed

* chore: Disable file log for debug build

* fix: Entry always displayed on default category

* refactor: Specify id, source, and url directly from MangaImpl constructor

* refactor: Make LibraryManga not extend MangaImpl

* refactor: Separate placeholder from LibraryManga

* fix: Default category should always be at the very beginning

* fix: Accidentally made the entries invisible

* fix: Default category disappear everytime a new category is added
2025-01-05 18:15:34 +07:00
e415fd4ef2
chore(about): Link weblate 2025-01-03 11:08:29 +07:00
a3672be728
fix(myanimelist): Fix nullability and fallback to medium cover if large
cover is null
2025-01-03 10:44:38 +07:00
e06b28a60e
fix: Handle version check for AboutController 2025-01-03 09:26:49 +07:00
eba5aa1d2e
fix: Make the markdown text scrollable 2025-01-03 08:40:46 +07:00
a554c079fb
fix: Handle update checker separately for AboutController for noe 2025-01-02 22:01:37 +07:00
49b10c1b4f
refactor: Rework Dialog 2025-01-02 21:42:53 +07:00
1a16d84e61
refactor(archive): Turn timespec function to extension method 2025-01-02 10:05:26 +07:00
fc87410d46
fix(download): Making sure archive tmp file is created properly 2025-01-02 09:05:41 +07:00
84d2924a82
chore(archive): Mark as internal or private 2025-01-02 08:29:30 +07:00
03e1953c9f
sync: Merge remote-tracking branch 'weblate/master' 2025-01-01 19:13:08 +07:00
Hosted Weblate
02c2b370b4
i18n: Translations update from Hosted Weblate
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: zhongfly <icesshadows@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai-plurals/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai-plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fil/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/id/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hant/
Translation: Yōkai/Yōkai
Translation: Yōkai/Yōkai Plurals
2025-01-01 19:05:27 +07:00
fa84ce8fe8
refactor: Reduce dependant towards RecentsPresenter 2025-01-01 14:02:57 +07:00
42dd857d94
docs: Sync changelog 2025-01-01 10:33:37 +07:00
2cf2fcfc4f
refactor: WIP delegated source refactor
J2K only handles deep link, which was disabled when I forked it as Yokai... Might gonna re-introduce it for some sources I used later (mainly Cubari tbh)
2025-01-01 10:28:00 +07:00
b4377a4609
refactor: Move archive related code to core.archive module 2025-01-01 09:26:15 +07:00
54a3059730
chore: Move core module to core.main 2025-01-01 08:21:55 +07:00
672d364f43
chore: Remove stdlib 2025-01-01 08:03:30 +07:00
b19480de1a
chore: Remove cmake arguments 2025-01-01 07:58:43 +07:00
3c00a249c3
fix: Resolve deprecated gradle function 2025-01-01 07:54:15 +07:00
Hosted Weblate
012407aede
Translations update from Hosted Weblate
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: zhongfly <icesshadows@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai-plurals/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai-plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/fil/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/id/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/yokai/yokai/zh_Hant/
Translation: Yōkai/Yōkai
Translation: Yōkai/Yōkai Plurals
2025-01-01 01:44:31 +01:00
1461f048dd
fix: Resolve deprecated gradle function 2025-01-01 07:44:09 +07:00
85af94d810
fix: Fix build 2025-01-01 07:35:55 +07:00
d02f1bdd11
refactor: Don't use context receiver
Deprecated on Kotlin 2.x, scheduled for removal in v2.1.x, will be replaced with context parameters

REF: https://github.com/Kotlin/KEEP/issues/259#issuecomment-2278319746
REF: https://youtrack.jetbrains.com/issue/KT-67119/Migration-warning-from-context-receivers-to-context-parameters
REF: https://github.com/Kotlin/KEEP/issues/367
2025-01-01 07:10:30 +07:00
1b92ae2e5f
chore: License and FIXME note 2024-12-31 12:53:37 +07:00
ac0d2e9fc0
revert: "fix: Emit scrolled event from onPreScroll" 2024-12-31 12:46:33 +07:00
8a28d1d484
fix: Emit scrolled event from onPreScroll 2024-12-31 12:24:45 +07:00
3399d6a326
docs: Remove Translation section [skip ci]
Handled by Weblate
2024-12-30 20:30:49 +07:00
6747795690
refactor: Use fast*() util functions 2024-12-30 20:28:44 +07:00
e554513392
style: Scrollbar
And add fast scroller component for later
2024-12-30 20:05:28 +07:00
f7e5abba59
ci: Ignore i18n changes except for base strings 2024-12-29 10:40:09 +07:00
a7874f2f29
docs: Update README [skip ci] 2024-12-29 10:36:09 +07:00
2f2ccac8e7
docs: Weblate [skip ci] 2024-12-29 10:08:20 +07:00
dd6a2f377a
docs: Sync changelog [skip ci] 2024-12-29 09:58:50 +07:00
f8807f81b1
chore: Remove unused translation strings [skip ci] 2024-12-29 09:48:00 +07:00
7ee9c7a746
docs(i18n): Add README.md [skip ci] 2024-12-29 09:27:44 +07:00
67c4500cce
style(about): Use CrossfadeTransition
It's basically the same as J2K's .withFadeTransaction
2024-12-28 12:15:08 +07:00
cd4079aa4b
fix: Crashes caused by cab40214d2
Screen arguments need to be parcelable
2024-12-28 11:23:34 +07:00
c6a86d773b
fix: Copy paste moment, it's Build time not Version 2024-12-28 11:08:48 +07:00
cab40214d2
refactor: Use Compose for About page 2024-12-28 10:53:01 +07:00
37f1f0e330
chore: Remove unused util function 2024-12-28 08:56:27 +07:00
310e90beb5
fix: Make status bar transparent 2024-12-27 10:26:23 +07:00
9bb869111d
fix: Don't hardcode status bar color 2024-12-27 08:55:03 +07:00
8a9d8166af
style(AppBar): Partially revert 71dcb2ab but adjust the color to
primaryContainer
2024-12-27 07:54:23 +07:00
71dcb2ab85
fix(AppBar): Adjust scrolled container color 2024-12-27 07:09:59 +07:00
60ef9482c8
style(AppBar): Scrolled container color 2024-12-26 13:55:43 +07:00
1cc8abb599
chore: Adjust aspect ratio to 2:3
For easier migration to Compose
2024-12-26 08:23:59 +07:00
96348dbf7d
fix(browse): Disable stable id
This reverts commit 8ac8187977.
2024-12-26 08:06:37 +07:00
23d4fb1fdd
fix(AppBar): Actions aren't aligned properly 2024-12-26 07:56:15 +07:00
f78d4e9e6a
fix(AppBar): Sizing issue when user flick too hard 2024-12-25 21:08:23 +07:00
120d2cfb96
feat(AppBar): EnterAlwaysCollapsed for Compose
Google left out EnterAlwaysCollapsed for some reason
2024-12-25 16:19:59 +07:00
dec1a70091
fix(AppBar): Re-introduce snap but only do it to the top bar 2024-12-25 16:12:11 +07:00
448c93365a
refactor: Try to mimic ExpandedAppBarLayout for Compose 2024-12-25 13:21:42 +07:00
55fad67223
fix(EmptyScreen): Align center the message if it's not tablet UI 2024-12-25 12:02:39 +07:00
c09c4045e2
refactor: Rework buildSrc
Co-authored-by: AntsyLich <59261191+antsylich@users.noreply.github.com>
2024-12-25 10:37:47 +07:00
b201e410a3
chore: Remove deprecated constants
They're moved to Constants object
2024-12-25 07:24:22 +07:00
915debd41b
docs: Sync changelog [skip ci] 2024-12-24 14:00:19 +07:00
6d2ae386a2
fix(reader): Use LocalContentColor's value 2024-12-24 13:41:37 +07:00
a853deae4a
fix(reader): Use it directly 2024-12-24 12:58:39 +07:00
f9bb2b96cb
fix(reader): Fix build 2024-12-24 12:43:30 +07:00
031e30e227
refactor: Use Compose for reader chapter transition
Co-authored-by: arkon <arkon@users.noreply.github.com>
2024-12-24 12:19:24 +07:00
0049653355
chore: Remove ic_local_library_24dp
No longer used, replaced by ImageVector
2024-12-24 08:50:53 +07:00
677d96eed5
docs: Sync changelog 2024-12-24 08:22:59 +07:00
640feb69ac
fix(recents): Only set list on queue state change when it's not empty 2024-12-24 08:07:10 +07:00
f3cac7cac8
refactor: Transform LocalSource icon from XML vector to ImageVector 2024-12-24 07:05:01 +07:00
99bec41056
style(EmptyScreen): Adjust spacing 2024-12-23 21:29:56 +07:00
afb7e79ea4
fix(EmptyScreen): Align buttons center 2024-12-23 21:17:45 +07:00
76af51a319
docs: Sync changelog 2024-12-23 21:06:20 +07:00
5824ac81c2
refactor: Use Compose for EmptyView 2024-12-23 20:58:30 +07:00
b1665eaedf
chore(i18n): Rephrasing 2024-12-23 09:26:45 +07:00
2b953a53d8
docs: Sync changelog 2024-12-23 09:12:53 +07:00
37f47ae2f5
fix(onboarding): Allow user to skip onboarding if Shizuku is installed
Fixes GH-322
2024-12-23 08:57:44 +07:00
3fa475c1cf
refactor: Move isShizukuInstalled check 2024-12-23 08:57:43 +07:00
renovate[bot]
bcf21858ee
fix(deps): Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1 (#321)
* fix(deps): Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1

* docs: Sync changelog

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ahmad Ansori Palembani <palembani@gmail.com>
2024-12-23 08:29:38 +07:00
90f5dfc55a
fix(library): Don't show Default category if it's empty 2024-12-23 07:33:23 +07:00
Ahmad Ansori Palembani
365d259e94
refactor(library): Utilise flow even more (#272)
* revert: "revert: "refactor(library): Some adjustments""

This reverts commit 2b639d0630.

* fix: Don't use emptyFlow

* fix: Fix build

* fix: Don't overwrite allCategories inside `combine {}`

* fix: Fix build
2024-12-23 07:07:11 +07:00
379 changed files with 8220 additions and 4714 deletions

View file

@ -1,13 +1,28 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style=space
insert_final_newline=true
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{json,json5}]
indent_size=2
[*.xml]
indent_size = 4
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
indent_size=4
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
indent_size = 4
max_line_length = 120
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_discouraged-comment-location = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled

View file

@ -35,9 +35,24 @@ body:
required: true
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
required: true
- label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**.
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
required: true
- label: I have checked through the app settings for my feature.
required: true
- label: I will fill out all of the requested information in this form.
required: true
- type: "textarea"
id: "prioritisation"
attributes:
label: "Is this issue important to you?"
description: |
**Please do not modify this text area!**
This template let users to vote with a :+1: reaction if they find it important.
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
value: |
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View file

@ -94,15 +94,30 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help.
- label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help ([Browse → Extensions → Find the extension → Settings → Tap the `[<>]` icon](https://cdn.aap.my.id/extension-repo-link.png), it *should* redirect you to the maintainer).
required: true
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon)
- label: I am reporting an issue exclusive to this fork. I have also checked that is not an issue on the [main version of Mihon](https://github.com/mihonapp/mihon).
required: true
- label: I have tried the [troubleshooting guide](https://mihon.app/help/).
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[1.9.7](https://github.com/null2264/yokai/releases/latest)**.
- label: I have updated the app to version **[1.9.7.3](https://github.com/null2264/yokai/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true
- label: I have filled out all of the requested information in this form.
required: true
- type: "textarea"
id: "prioritisation"
attributes:
label: "Is this issue important to you?"
description: |
**Please do not modify this text area!**
This template let users to vote with a :+1: reaction if they find it important.
This is not a guarantee that highly-requested issues will be fixed first, but it helps us to figure out what's important to users. Please react on other users' issues if you find them important.
value: |
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/null2264/yokai/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,11 @@
<!--
^ Please summarise the changes you have made here ^
-->
---
Add a :+1: [reaction] to [pull requests you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[pull requests you find important]: https://github.com/null2264/yokai/pulls?q=is%3Aopen+sort%3Areactions-%2B1-desc

View file

@ -4,7 +4,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [pull_request]
on:
pull_request:
paths:
- '**'
- '!**.md'
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
- 'i18n/src/commonMain/moko-resources/base/plurals.xml'
jobs:
build:
@ -22,18 +30,21 @@ jobs:
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
- name: Setup Gradle
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
with:
java: 17
distro: adopt
distro: temurin
- name: Copy CI gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build and run tests
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Build the app
run: ./gradlew assembleStandardRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest testStandardReleaseUnitTest
- name: Publish test report
uses: mikepenz/action-junit-report@v5

View file

@ -43,10 +43,10 @@ jobs:
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
- name: Setup Gradle
uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
uses: null2264/actions/gradle-java-setup@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
with:
java: 17
distro: adopt
distro: temurin
- name: Setup CHANGELOG parser
uses: taiki-e/install-action@parse-changelog
@ -80,6 +80,7 @@ jobs:
run: |
set -x
echo "VERSION_TAG=v${{github.event.inputs.version}}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardRelease" >> $GITHUB_ENV
# BETA
- name: Prepare beta build
@ -88,9 +89,14 @@ jobs:
set -x
BETA_COUNT=$(git tag -l --sort=refname "v${{github.event.inputs.version}}-b*" | tail -n1 | sed "s/^\S*-b//g")
[ "$BETA_COUNT" = "" ] && BETA_COUNT="1" || BETA_COUNT=$((BETA_COUNT+1))
if [ -z "$BETA_COUNT" ]; then
BETA_COUNT="1"
else
BETA_COUNT=$((BETA_COUNT+1))
fi
echo "VERSION_TAG=v${{github.event.inputs.version}}-b${BETA_COUNT}" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardBeta" >> $GITHUB_ENV
# NIGHTLY
- name: Prepare nightly build
@ -98,23 +104,17 @@ jobs:
run: |
set -x
echo "VERSION_TAG=r$(git rev-list --count HEAD)" >> $GITHUB_ENV
echo "BUILD_TYPE=StandardNightly" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
echo "COMMIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# PROD
- name: Build release build and run tests
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta != 'true'
run: ./gradlew assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Build the app
if: startsWith(env.BUILD_TYPE, 'Standard')
run: ./gradlew assemble${{ env.BUILD_TYPE }}
# BETA
- name: Build beta build and run tests
if: startsWith(env.VERSION_TAG, 'v') && github.event.inputs.beta == 'true'
run: ./gradlew assembleStandardBeta testReleaseUnitTest testStandardBetaUnitTest
# NIGHTLY
- name: Build nightly build and run tests
if: startsWith(env.VERSION_TAG, 'r')
run: ./gradlew assembleStandardNightly testReleaseUnitTest testStandardNightlyUnitTest
- name: Run unit tests
if: startsWith(env.BUILD_TYPE, 'Standard')
run: ./gradlew testReleaseUnitTest test${{ env.BUILD_TYPE }}UnitTest
- name: Upload R8 APK to artifact
uses: actions/upload-artifact@v4
@ -154,7 +154,7 @@ jobs:
echo "STAGE=${stage}" >> $GITHUB_OUTPUT
- name: Sign APK
uses: null2264/actions/android-signer@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29
uses: null2264/actions/android-signer@363cb9cf3d66bd9c72ed6860142c6b2c121d7e94
if: env.VERSION_TAG != ''
with:
releaseDir: app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}

1
.gitignore vendored
View file

@ -10,3 +10,4 @@
*/*/build
.kotlin/
kls_database.db
weblate.conf

View file

@ -24,6 +24,8 @@
'com.github.tachiyomiorg:image-decoder',
'com.github.tachiyomiorg:unifile',
'com.github.tachiyomiorg:conductor-support-preference',
'com.github.chrisbanes:PhotoView',
'com.github.PhilJay:MPAndroidChart',
],
enabled: false,
},

View file

@ -6,9 +6,101 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
- `Additions` - New features
- `Changes` - Behaviour/visual changes
- `Fixes` - Bugfixes
- `Translation` - Translation changes/updates
- `Other` - Technical changes/updates
## [Unreleased]
### Additions
- Add random library sort
- Add the ability to save search queries
- Add toggle to enable/disable hide source on swipe (@Hiirbaf)
- Add the ability to mark duplicate read chapters as read (@AntsyLich)
### Changes
- Temporarily disable log file
- Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03)
- Chapter progress now saved everything the page is changed
### Fixes
- Allow users to bypass onboarding's permission step if Shizuku is installed
- Fix Recents page shows "No recent chapters" instead of a loading screen
- Fix not fully loaded entries can't be selected on Library page
- Fix certain Infinix devices being unable to use any "Open link in browser" actions, including tracker setup (@MajorTanya)
- Fix source filter bottom sheet unable to be fully scrolled to the bottom
- Prevent potential "Comparison method violates its general contract!" crash
- Fix staggered grid cover being squashed for local source (@AwkwardPeak7)
### Translation
- Update translations from Weblate
### Other
- Refactor Library to utilize Flow even more
- Refactor EmptyView to use Compose
- Refactor Reader ChapterTransition to use Compose (@arkon)
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
- Refactor About page to use Compose
- Adjust Compose-based pages' transition to match J2K's Conductor transition
- Resolve deprecation warnings
- Kotlin's context-receiver, schedule for removal on Kotlin v2.1.x and planned to be replaced by context-parameters on Kotlin v2.2
- Project.exec -> Providers.exec
- Remove internal API usage to retrieve Kotlin version for kotlin-stdlib
- Move :core module to :core:main
- Move archive related code to :core:archive (@AntsyLich)
- Refactor Library to store LibraryMap instead of flatten list of LibraryItem
- LibraryItem abstraction to make it easier to manage
- LibraryManga no longer extend MangaImpl
- Update dependency gradle to v8.12
- Update user agent (@Hiirbaf)
- Update serialization to v1.8.1
- Update dependency io.github.fornewid:material-motion-compose-core to v1.2.1
- Update lifecycle to v2.9.0
- Update dependency org.jsoup:jsoup to v1.20.1
- Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0
- Update dependency io.mockk:mockk to v1.14.2
- Update dependency io.coil-kt.coil3:coil-bom to v3.2.0
- Update dependency com.squareup.okio:okio to v3.12.0
- Update dependency com.google.firebase:firebase-bom to v33.14.0
- Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.36.0
- Update dependency com.github.requery:sqlite-android to v3.49.0
- Update dependency com.getkeepsafe.taptargetview:taptargetview to v1.15.0
- Update dependency androidx.window:window to v1.4.0
- Update dependency androidx.webkit:webkit to v1.13.0
- Update dependency androidx.sqlite:sqlite-ktx to v2.5.1
- Update dependency androidx.sqlite:sqlite to v2.5.1
- Update dependency androidx.recyclerview:recyclerview to v1.4.0
- Update dependency androidx.core:core-ktx to v1.16.0
- Update dependency androidx.compose:compose-bom to v2025.05.01
- Update aboutlibraries to v11.6.3
- Update plugin kotlinter to v5.1.0
- Update plugin gradle-versions to v0.52.0
- Update okhttp monorepo to v5.0.0-alpha.16
- Update moko to v0.24.5
- Update kotlin monorepo to v2.1.21
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.2
- Update dependency me.zhanghai.android.libarchive:library to v1.1.5
- Update dependency io.insert-koin:koin-bom to v4.0.4
- Update dependency com.android.tools:desugar_jdk_libs to v2.1.5
- Update dependency androidx.work:work-runtime-ktx to v2.10.1
- Update dependency androidx.constraintlayout:constraintlayout to v2.2.1
- Update plugin firebase-crashlytics to v3.0.3
- Update null2264/actions digest to 363cb9c
- Update dependency io.github.pdvrieze.xmlutil:core-android to v0.91.1
## [1.9.7.3]
### Fixes
- More `Comparison method violates its general contract!` crash prevention
## [1.9.7.2]
### Fixes
- Fix MyAnimeList timeout issue
## [1.9.7.1]
### Fixes
- Prevent `Comparison method violates its general contract!` crashes
## [1.9.7]
### Changes

View file

@ -12,10 +12,13 @@
A free and open source manga reader
[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg)](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](/LICENSE)
[![Discord: Mihon](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg)](https://gitlab.com/null2264/yokai)
[![Mirror: GitLab](https://img.shields.io/badge/mirror-GitLab-orange.svg?labelColor=27303D)](https://gitlab.com/null2264/yokai)
[![Mirror: git.aap](https://img.shields.io/badge/mirror-git.aap-red.svg?labelColor=27303D)](https://git.aap.my.id/null2264/yokai)
[![CI](https://github.com/null2264/yokai/actions/workflows/build_push.yml/badge.svg?labelColor=27303D)](https://github.com/null2264/yokai/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/null2264/yokai?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/yokai?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/yokai/)
<img src="./.github/readme-images/screens.gif" alt="Yokai screenshots" />

View file

@ -1,15 +1,13 @@
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPlugin
import com.google.gms.googleservices.GoogleServicesPlugin
import java.io.ByteArrayOutputStream
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(androidx.plugins.application)
alias(kotlinx.plugins.android)
alias(kotlinx.plugins.compose.compiler)
id("yokai.android.application")
id("yokai.android.application.compose")
alias(kotlinx.plugins.serialization)
alias(kotlinx.plugins.parcelize)
alias(libs.plugins.aboutlibraries)
@ -23,15 +21,12 @@ if (gradle.startParameter.taskRequests.toString().contains("standard", true)) {
}
fun runCommand(command: String): String {
val byteOut = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
val result = providers.exec { commandLine(command.split(" ")) }
return result.standardOutput.asText.get().trim()
}
val _versionName = "1.9.8"
@Suppress("PropertyName")
val _versionName = "1.10.0"
val betaCount by lazy {
val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*")
@ -54,7 +49,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 157
versionCode = 158
versionName = _versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = true
@ -72,11 +67,6 @@ android {
//noinspection ChromeOsAbiSupport
abiFilters += supportedAbis
}
externalNativeBuild {
cmake {
this.arguments("-DHAVE_LIBJXL=FALSE")
}
}
}
splits {
@ -122,7 +112,6 @@ android {
buildFeatures {
viewBinding = true
compose = true
// If you're here because there's not BuildConfig, build the app first, it'll generate it for you
buildConfig = true
@ -156,7 +145,8 @@ android {
}
dependencies {
implementation(projects.core)
implementation(projects.core.archive)
implementation(projects.core.main)
implementation(projects.data)
implementation(projects.domain)
implementation(projects.i18n)
@ -251,8 +241,6 @@ dependencies {
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
@ -280,17 +268,15 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
// "-opt-in=kotlin.Experimental",
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=kotlin.ExperimentalStdlibApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
// "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
// "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
@ -298,19 +284,6 @@ tasks {
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
)
compilerOptions.freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
(project.layout.buildDirectory.asFile.orNull?.absolutePath ?: "/tmp/yokai") + "/compose_metrics",
)
}
}
// Duplicating Hebrew string assets due to some locale code issues on different devices

View file

@ -300,7 +300,7 @@ fun buildLogWritersToAdd(
) = buildList {
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
if (logPath != null) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
// if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
}
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"

View file

@ -40,9 +40,13 @@ import eu.kanade.tachiyomi.util.system.launchIO
import java.util.Calendar
import java.util.Date
import kotlin.math.min
import kotlin.math.roundToLong
import kotlinx.coroutines.MainScope
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.models.cover
import yokai.domain.recents.interactor.GetRecents
class UpdatesGridGlanceWidget : GlanceAppWidget() {
private val app: Application by injectLazy()
@ -64,6 +68,33 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
}
}
// FIXME: Don't depends on RecentsPresenter
private suspend fun getUpdates(customAmount: Int = 0, getRecents: GetRecents = Injekt.get()): List<Pair<Manga, Long>> {
return getRecents
.awaitUpdates(
limit = when {
customAmount > 0 -> (customAmount * 1.5).roundToLong()
else -> 25L
}
)
.mapNotNull {
when {
it.chapter.read || it.chapter.id == null -> RecentsPresenter.getNextChapter(it.manga)
it.history.id == null -> RecentsPresenter.getFirstUpdatedChapter(it.manga, it.chapter)
else -> it.chapter
} ?: return@mapNotNull null
it
}
.asSequence()
.distinctBy { it.manga.id }
.sortedByDescending { it.history.last_read }
// nChapterItems + nAdditionalItems + cReadingItems
.take((RecentsPresenter.UPDATES_CHAPTER_LIMIT * 2) + RecentsPresenter.UPDATES_READING_LIMIT_LOWER)
.filter { it.manga.id != null }
.map { it.manga to it.history.last_read }
.toList()
}
fun loadData(list: List<Pair<Manga, Long>>? = null) {
coroutineScope.launchIO {
// Don't show anything when lock is active
@ -80,7 +111,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount))
val processList = list ?: getUpdates(customAmount = min(50, rowCount * columnCount))
data = prepareList(processList, rowCount * columnCount)
ids.forEach { update(app, it) }

View file

@ -17,15 +17,15 @@ import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import eu.kanade.tachiyomi.appwidget.ContainerModifier
import eu.kanade.tachiyomi.appwidget.util.stringResource
import eu.kanade.tachiyomi.ui.main.MainActivity
import yokai.i18n.MR
import yokai.presentation.core.Constants
@Composable
fun LockedWidget() {
val context = LocalContext.current
val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
Box(

View file

@ -17,19 +17,18 @@ import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.text.Text
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import eu.kanade.tachiyomi.appwidget.ContainerModifier
import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount
import eu.kanade.tachiyomi.appwidget.util.stringResource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity
import yokai.i18n.MR
import yokai.presentation.core.Constants
@Composable
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS)
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(Constants.SHORTCUT_RECENTS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
Column(
modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)),

View file

@ -5,9 +5,17 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import eu.kanade.tachiyomi.core.preference.Preference
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
}
fun String.asDateFormat(): DateFormat = when (this) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(this, Locale.getDefault())
}

View file

@ -57,8 +57,10 @@ data class BackupManga(
@ProtoNumber(805) var customGenre: List<String>? = null,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
url = this@BackupManga.url
return MangaImpl(
source = this.source,
url = this.url,
).apply {
title = this@BackupManga.title
artist = this@BackupManga.artist
author = this@BackupManga.author
@ -67,7 +69,6 @@ data class BackupManga(
status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded
viewer_flags = (
this@BackupManga.viewer_flags

View file

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
import yokai.i18n.MR
import yokai.util.lang.getString
import java.io.Serializable
interface Category : Serializable {
@ -37,8 +37,7 @@ interface Category : Serializable {
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
}
fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort)
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
fun sortingMode(): LibrarySort? = LibrarySort.valueOf(mangaSort)
val isDragAndDrop
get() = (
@ -56,7 +55,21 @@ interface Category : Serializable {
fun mangaOrderToString(): String =
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/")
// For dynamic categories
fun dynamicHeaderKey(): String {
if (!isDynamic) throw IllegalStateException("This category is not a dynamic category")
return when {
sourceId != null -> "${name}$sourceSplitter${sourceId}"
langId != null -> "${langId}$langSplitter${name}"
else -> name
}
}
companion object {
const val sourceSplitter = "◘•◘"
const val langSplitter = "⨼⨦⨠"
var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply {

View file

@ -32,10 +32,14 @@ class CategoryImpl : Category {
val category = other as Category
if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey()
return name == category.name
}
override fun hashCode(): Int {
if (isDynamic) return dynamicHeaderKey().hashCode()
return name.hashCode()
}
}

View file

@ -4,11 +4,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
fun SChapter.toChapter(): ChapterImpl {
return ChapterImpl().apply {
name = this@SChapter.name
url = this@SChapter.url
date_upload = this@SChapter.date_upload
chapter_number = this@SChapter.chapter_number
scanlator = this@SChapter.scanlator
name = this@toChapter.name
url = this@toChapter.url
date_upload = this@toChapter.date_upload
chapter_number = this@toChapter.chapter_number
scanlator = this@toChapter.scanlator
}
}

View file

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.domain.manga.models.Manga
import kotlin.math.roundToInt
import yokai.data.updateStrategyAdapter
data class LibraryManga(
val manga: Manga,
var unread: Int = 0,
var read: Int = 0,
var category: Int = 0,
@ -13,41 +13,11 @@ data class LibraryManga(
var latestUpdate: Long = 0,
var lastRead: Long = 0,
var lastFetch: Long = 0,
) : MangaImpl() {
var realMangaCount = 0
get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders")
set(value) {
if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders")
field = value
}
) {
val hasRead
get() = read > 0
@Transient
var items: List<LibraryItem>? = null
get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders")
set(value) {
if (!isHidden()) throw IllegalStateException("items can only be set by placeholders")
field = value
}
companion object {
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
title = ""
id = Long.MIN_VALUE
category = categoryId
}
fun createHide(categoryId: Int, title: String, hiddenItems: List<LibraryItem>): LibraryManga =
createBlank(categoryId).apply {
this.title = title
this.status = -1
this.read = hiddenItems.size
this.items = hiddenItems
}
fun mapper(
// manga
id: Long,
@ -78,34 +48,37 @@ data class LibraryManga(
latestUpdate: Long,
lastRead: Long,
lastFetch: Long,
): LibraryManga = createBlank(categoryId.toInt()).apply {
this.id = id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite
this.last_update = lastUpdate ?: 0L
this.initialized = initialized
this.viewer_flags = viewerFlags.toInt()
this.hide_title = hideTitle
this.chapter_flags = chapterFlags.toInt()
this.date_added = dateAdded ?: 0L
this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
this.cover_last_modified = coverLastModified
this.read = readCount.roundToInt()
this.unread = maxOf((total - readCount).roundToInt(), 0)
this.totalChapters = total.toInt()
this.bookmarkCount = bookmarkCount.roundToInt()
this.latestUpdate = latestUpdate
this.lastRead = lastRead
this.lastFetch = lastFetch
}
): LibraryManga = LibraryManga(
manga = Manga.mapper(
id = id,
source = source,
url = url,
artist = artist,
author = author,
description = description,
genre = genre,
title = title,
status = status,
thumbnailUrl = thumbnailUrl,
favorite = favorite,
lastUpdate = lastUpdate,
initialized = initialized,
viewerFlags = viewerFlags,
hideTitle = hideTitle,
chapterFlags = chapterFlags,
dateAdded = dateAdded,
filteredScanlators = filteredScanlators,
updateStrategy = updateStrategy,
coverLastModified = coverLastModified,
),
read = readCount.roundToInt(),
unread = maxOf((total - readCount).roundToInt(), 0),
totalChapters = total.toInt(),
bookmarkCount = bookmarkCount.roundToInt(),
category = categoryId.toInt(),
latestUpdate = latestUpdate,
lastRead = lastRead,
lastFetch = lastFetch,
)
}
}

View file

@ -182,15 +182,13 @@ var Manga.vibrantCoverColor: Int?
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
}
fun Manga.Companion.create(source: Long) = MangaImpl().apply {
this.source = source
}
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
MangaImpl(
source = source,
url = url,
).apply {
this.title = title
}
fun Manga.Companion.mapper(
id: Long,
@ -213,14 +211,12 @@ fun Manga.Companion.mapper(
filteredScanlators: String?,
updateStrategy: Long,
coverLastModified: Long,
) = create(source).apply {
) = create(url, title, source).apply {
this.id = id
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite

View file

@ -12,7 +12,11 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
companion object {
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
fun createBlank() = MangaChapterHistory(
MangaImpl(null, -1, ""),
ChapterImpl(),
HistoryImpl(),
)
fun mapper(
// manga

View file

@ -8,13 +8,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import uy.kohesive.injekt.injectLazy
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Long = -1
override lateinit var url: String
open class MangaImpl(
override var id: Long? = null,
override var source: Long = -1,
override var url: String = "",
) : Manga {
private val customMangaManager: CustomMangaManager by injectLazy()
@ -107,7 +105,7 @@ open class MangaImpl : Manga {
}
override fun hashCode(): Int {
return if (::url.isInitialized) {
return if (url.isNotBlank()) {
url.hashCode()
} else {
(id ?: 0L).hashCode()

View file

@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.download.DownloadPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
@ -171,7 +171,7 @@ class DownloadManager(
return files.sortedBy { it.name }
.mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.State.READY }
Page(i, uri = file.uri).apply { status = Page.State.Ready }
}
}

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.os.Looper
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.ChapterCache
@ -55,8 +54,8 @@ import kotlinx.coroutines.supervisorScope
import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.ZipWriter
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
@ -365,11 +364,11 @@ class Downloader(
// Get all the URLs to the source images, fetch pages if necessary
pageList.filter { it.imageUrl.isNullOrEmpty() }.forEach { page ->
page.status = Page.State.LOAD_PAGE
page.status = Page.State.LoadPage
try {
page.imageUrl = download.source.getImageUrl(page)
} catch (e: Throwable) {
page.status = Page.State.ERROR
page.status = Page.State.Error
}
}
@ -494,12 +493,12 @@ class Downloader(
page.uri = file.uri
page.progress = 100
page.status = Page.State.READY
page.status = Page.State.Ready
} catch (e: Throwable) {
if (e is CancellationException) throw e
// Mark this page as error and allow to download the remaining
page.progress = 0
page.status = Page.State.ERROR
page.status = Page.State.Error
notifier.onError(e.message, chapName, download.manga.title)
}
}
@ -518,7 +517,7 @@ class Downloader(
tmpDir: UniFile,
filename: String,
): UniFile {
page.status = Page.State.DOWNLOAD_IMAGE
page.status = Page.State.DownloadImage
page.progress = 0
return flow {
val response = source.getImage(page)
@ -604,8 +603,9 @@ class Downloader(
dirname: String,
tmpDir: UniFile,
) {
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
ZipWriter(context, zip).use { writer ->
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
if (zip?.isFile != true) throw Exception("Failed to create CBZ file for downloaded chapter")
ZipWriter(context, zip!!).use { writer ->
tmpDir.listFiles()?.forEach { file ->
writer.write(file)
}

View file

@ -22,7 +22,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
get() = pages?.sumOf(Page::progress) ?: 0
val downloadedImages: Int
get() = pages?.count { it.status == Page.State.READY } ?: 0
get() = pages?.count { it.status is Page.State.Ready } ?: 0
@Transient
private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED)

View file

@ -4,6 +4,7 @@ import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@ -26,7 +27,6 @@ import yokai.domain.library.custom.interactor.GetCustomManga
import yokai.domain.library.custom.interactor.RelinkCustomManga
import yokai.domain.library.custom.model.CustomMangaInfo
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import java.nio.charset.StandardCharsets
class CustomMangaManager(val context: Context) {
private val scope = CoroutineScope(Dispatchers.IO)
@ -176,8 +176,7 @@ class CustomMangaManager(val context: Context) {
val status: Int? = null,
) {
fun toManga() = MangaImpl().apply {
id = this@MangaJson.id
fun toManga() = MangaImpl(id = this.id).apply {
title = this@MangaJson.title ?: ""
author = this@MangaJson.author
artist = this@MangaJson.artist
@ -272,9 +271,6 @@ class CustomMangaManager(val context: Context) {
}
}
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply {
this.id = id
this.copyFromComicInfo(comicInfo)
this.title = comicInfo.series?.value ?: ""
}
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) =
MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) }
}

View file

@ -173,16 +173,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val mangaList = (
if (savedMangasList != null) {
val mangas = getLibraryManga.await().filter {
it.id in savedMangasList
}.distinctBy { it.id }
val mangas =
getLibraryManga.await()
.filter { it.manga.id in savedMangasList }
.distinctBy { it.manga.id }
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
if (categoryId > -1) categoryIds.add(categoryId)
mangas
} else {
getMangaToUpdate()
}
).sortedBy { it.title }
).sortedBy { it.manga.title }
return withIOContext {
try {
@ -227,7 +228,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates.
mangaToUpdate.addAll(mangaToAdd)
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source })
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source })
checkIfMassiveUpdate()
coroutineScope {
val list = mangaToUpdateMap.keys.map { source ->
@ -257,42 +258,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list ->
val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list ->
async {
requestSemaphore.withPermit {
list.forEach { manga ->
ensureActive()
val source = sourceManager.get(manga.source) as? HttpSource ?: return@async
val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async
notifier.showProgressNotification(
manga,
manga.manga,
count.andIncrement,
mangaToUpdate.size,
)
ensureActive()
val networkManga = try {
source.getMangaDetails(manga.copy())
source.getMangaDetails(manga.manga.copy())
} catch (e: java.lang.Exception) {
Logger.e(e)
null
}
if (networkManga != null) {
manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
manga.initialized = true
manga.manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.manga.thumbnail_url
manga.manga.copyFrom(networkManga)
manga.manga.initialized = true
val request: ImageRequest =
if (thumbnailUrl != manga.thumbnail_url) {
if (thumbnailUrl != manga.manga.thumbnail_url) {
// load new covers in background
ImageRequest.Builder(context).data(manga.cover())
ImageRequest.Builder(context).data(manga.manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED).build()
} else {
ImageRequest.Builder(context).data(manga.cover())
ImageRequest.Builder(context).data(manga.manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY)
.build()
}
context.imageLoader.execute(request)
updateManga.await(manga.toMangaUpdate())
updateManga.await(manga.manga.toMangaUpdate())
}
}
}
@ -313,9 +314,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val loggedServices = trackManager.services.filter { it.isLogged }
mangaToUpdate.forEach { manga ->
notifier.showProgressNotification(manga, count++, mangaToUpdate.size)
notifier.showProgressNotification(manga.manga, count++, mangaToUpdate.size)
val tracks = getTrack.awaitAllByMangaId(manga.id!!)
val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!)
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
@ -324,7 +325,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newTrack = service.refresh(track)
insertTrack.await(newTrack)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service)
} catch (e: Exception) {
Logger.e(e)
}
@ -376,7 +377,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private fun checkIfMassiveUpdate() {
val largestSourceSize = mangaToUpdate
.groupBy { it.source }
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
@ -391,7 +392,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val httpSource = sourceManager.get(source) as? HttpSource ?: return false
while (count < mangaToUpdateMap[source]!!.size) {
val manga = mangaToUpdateMap[source]!![count]
val shouldDownload = manga.shouldDownloadNewChapters(preferences)
val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences)
if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) {
hasDownloads = true
}
@ -410,15 +411,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
try {
var hasDownloads = false
ensureActive()
notifier.showProgressNotification(manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.copy())
notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.manga.copy())
if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source)
if (newChapters.first.isNotEmpty()) {
if (shouldDownload) {
downloadChapters(
manga,
manga.manga,
newChapters.first.sortedBy { it.chapter_number },
)
hasDownloads = true
@ -428,24 +429,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
if (deleteRemoved && newChapters.second.isNotEmpty()) {
val removedChapters = newChapters.second.filter {
downloadManager.isChapterDownloaded(it, manga) &&
downloadManager.isChapterDownloaded(it, manga.manga) &&
newChapters.first.none { newChapter ->
newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank()
}
}
if (removedChapters.isNotEmpty()) {
downloadManager.deleteChapters(removedChapters, manga, source)
downloadManager.deleteChapters(removedChapters, manga.manga, source)
}
}
if (newChapters.first.size + newChapters.second.size > 0) {
sendUpdate(manga.id)
sendUpdate(manga.manga.id)
}
}
return@coroutineScope hasDownloads
} catch (e: Exception) {
if (e !is CancellationException) {
failedUpdates[manga] = e.message
Logger.e { "Failed updating: ${manga.title}: $e" }
failedUpdates[manga.manga] = e.message
Logger.e { "Failed updating: ${manga.manga.title}: $e" }
}
return@coroutineScope false
}
@ -461,17 +462,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = preferences.libraryUpdateMangaRestriction().get()
return mangaToAdd.filter { manga ->
when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed)
MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed)
}
MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
}
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_started)
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_started)
}
manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update)
manga.manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update)
}
else -> {
return@filter true
@ -503,10 +504,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
categoryIds.addAll(categoriesToUpdate)
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id }
} else {
categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0)
libraryManga.distinctBy { it.id }
libraryManga.distinctBy { it.manga.id }
}
}
@ -564,13 +565,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
private fun addMangaToQueue(categoryId: Int, manga: List<LibraryManga>) {
val mangas = filterMangaToUpdate(manga).sortedBy { it.title }
val mangas = filterMangaToUpdate(manga).sortedBy { it.manga.title }
categoryIds.add(categoryId)
addManga(mangas)
}
private fun addCategory(categoryId: Int) {
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title }
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.manga.title }
categoryIds.add(categoryId)
addManga(mangas)
}
@ -579,7 +580,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val distinctManga = mangaToAdd.filter { it !in mangaToUpdate }
mangaToUpdate.addAll(distinctManga)
checkIfMassiveUpdate()
distinctManga.groupBy { it.source }.forEach {
distinctManga.groupBy { it.manga.source }.forEach {
// if added queue items is a new source not in the async list or an async list has
// finished running
if (mangaToUpdateMap[it.key].isNullOrEmpty()) {
@ -727,9 +728,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (mangaToUse != null) {
builder.putLongArray(
KEY_MANGAS,
mangaToUse.firstOrNull()?.id?.let { longArrayOf(it) } ?: longArrayOf(),
mangaToUse.firstOrNull()?.manga?.id?.let { longArrayOf(it) } ?: longArrayOf(),
)
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.id }
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.manga.id }
}
}
val inputData = builder.build()

View file

@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) {
val manga = it.key
val chapters = it.value
val chapterNames = chapters.map { chapter ->
chapter.preferredChapterName(context, manga, preferences)
chapter.preferredChapterName(context, manga.manga, preferences)
}
notifications.add(
Pair(
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_yokai)
try {
val request = ImageRequest.Builder(context).data(manga.cover())
val request = ImageRequest.Builder(context).data(manga.manga.cover())
.networkCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.transformations(CircleCropTransformation())
@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) {
} catch (_: Exception) {
}
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.title)
setContentTitle(manga.manga.title)
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) {
"${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " +
@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(
NotificationReceiver.openChapterPendingActivity(
context,
manga,
manga.manga,
chapters.first(),
),
)
@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
manga,
manga.manga,
chapters,
Notifications.ID_NEW_CHAPTERS,
),
@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.view_chapters),
NotificationReceiver.openChapterPendingActivity(
context,
manga,
manga.manga,
Notifications.ID_NEW_CHAPTERS,
),
)
setAutoCancel(true)
},
manga.id.hashCode(),
manga.manga.id.hashCode(),
),
)
}
@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle()
.bigText(
updates.keys.joinToString("\n") {
it.title.chop(45)
it.manga.title.chop(45)
},
),
)
}
} else if (!preferences.hideNotificationContent().get()) {
setContentText(updates.keys.first().title.chop(45))
setContentText(updates.keys.first().manga.title.chop(45))
}
priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS)

View file

@ -487,7 +487,7 @@ class NotificationReceiver : BroadcastReceiver() {
*/
internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
val newIntent =
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
.putExtra(Constants.MANGA_EXTRA, manga.id)
.putExtra("notificationId", manga.id.hashCode())

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
@ -19,13 +20,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.Themes
import java.text.DateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@ -187,10 +187,10 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun dateFormatRaw() = preferenceStore.getString(Keys.dateFormat, "")
@Deprecated("Use dateFormatRaw().get().asDateFormat() instead")
fun dateFormat(format: String = dateFormatRaw().get()): DateFormat = format.asDateFormat()
fun appLanguage() = preferenceStore.getString("app_language", "")

View file

@ -49,15 +49,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date))
}
}
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALAddMangaResult>()
.let {
track.library_id = it.data.entry.id
track
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALAddMangaResult>()
.let {
track.library_id = it.data.entry.id
track
}
}
}
@ -74,11 +72,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date))
}
}
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
}
@ -90,13 +86,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search)
}
}
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALSearchResult>()
.data.page.media
.map { it.toALManga().toTrack() }
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALSearchResult>()
.data.page.media
.map { it.toALManga().toTrack() }
}
}
@ -109,15 +103,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id)
}
}
with(json) {
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALUserListMangaQueryResult>()
.data.page.mediaList
.map { it.toALUserManga() }
.firstOrNull()
?.toTrack()
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
.parseAs<ALUserListMangaQueryResult>()
.data.page.mediaList
.map { it.toALUserManga() }
.firstOrNull()
?.toTrack()
}
}

View file

@ -75,29 +75,25 @@ class BangumiApi(
.appendQueryParameter("responseGroup", "large")
.appendQueryParameter("max_results", "20")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<BGMSearchResult>()
.let { result ->
if (result.code == 404) emptyList<TrackSearch>()
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<BGMSearchResult>()
.let { result ->
if (result.code == 404) emptyList<TrackSearch>()
result.list
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
}
result.list
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
}
}
suspend fun findLibManga(track: Track): Track? {
return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
.awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
}
authClient.newCall(GET("$API_URL/subject/${track.media_id}"))
.awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
}
}
@ -111,29 +107,25 @@ class BangumiApi(
.build()
// TODO: get user readed chapter here
with(json) {
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
track.last_chapter_read = it.epStatus!!.toFloat()
track.score = it.rating!!.toFloat()
track
}
}
track.status = it.status?.id?.toInt() ?: Bangumi.DEFAULT_STATUS
track.last_chapter_read = it.epStatus!!.toFloat()
track.score = it.rating!!.toFloat()
track
}
}
}
suspend fun accessToken(code: String): BGMOAuth {
return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
}

View file

@ -44,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
try {
client.newCall(request).execute().use {
when (it.code) {
200 -> return with(json) { it.parseAs<AuthenticationDto>().token }
200 -> return it.parseAs<AuthenticationDto>().token
401 -> {
Logger.w { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
throw IOException("Unauthorized / api key not valid")
@ -89,11 +89,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
private fun getTotalChapters(url: String): Long {
val requestUrl = getApiVolumesUrl(url)
try {
val listVolumeDto = with(json) {
val listVolumeDto =
authClient.newCall(GET(requestUrl))
.execute()
.parseAs<List<VolumeDto>>()
}
var volumeNumber = 0L
var maxChapterNumber = 0L
for (volume in listVolumeDto) {
@ -117,9 +116,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
try {
authClient.newCall(GET(requestUrl)).execute().use {
if (it.code == 200) {
return with(json) {
it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
}
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
}
if (it.code == 204) {
return 0F
@ -134,11 +131,10 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
try {
val serieDto: SeriesDto = with(json) {
val serieDto: SeriesDto =
authClient.newCall(GET(url))
.awaitSuccess()
.parseAs()
}
val track = serieDto.toTrack()
track.apply {

View file

@ -131,14 +131,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
with(json) {
authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess()
.parseAs<KitsuSearchResult>()
.let {
algoliaSearch(it.media.key, query)
}
}
authClient.newCall(GET(ALGOLIA_KEY_URL))
.awaitSuccess()
.parseAs<KitsuSearchResult>()
.let {
algoliaSearch(it.media.key, query)
}
}
}
@ -147,25 +145,23 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val jsonObject = buildJsonObject {
put("params", "query=$query$ALGOLIA_FILTER")
}
with(json) {
client.newCall(
POST(
ALGOLIA_URL,
headers = headersOf(
"X-Algolia-Application-Id",
ALGOLIA_APP_ID,
"X-Algolia-API-Key",
key,
),
body = jsonObject.toString().toRequestBody(jsonMime),
client.newCall(
POST(
ALGOLIA_URL,
headers = headersOf(
"X-Algolia-Application-Id",
ALGOLIA_APP_ID,
"X-Algolia-API-Key",
key,
),
)
.awaitSuccess()
.parseAs<KitsuAlgoliaSearchResult>()
.hits
.filter { it.subtype != "novel" }
.map { it.toTrack() }
}
body = jsonObject.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
.parseAs<KitsuAlgoliaSearchResult>()
.hits
.filter { it.subtype != "novel" }
.map { it.toTrack() }
}
}
@ -175,18 +171,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
null
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
null
}
}
}
}
}
@ -196,18 +190,16 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
throw Exception("Could not find manga")
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuListSearchResult>()
.let {
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
it.firstToTrack()
} else {
throw Exception("Could not find manga")
}
}
}
}
}
@ -220,11 +212,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET)
.build()
with(json) {
client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess()
.parseAs()
}
client.newCall(POST(LOGIN_URL, body = formBody))
.awaitSuccess()
.parseAs()
}
}
@ -233,13 +223,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val url = "${BASE_URL}users".toUri().buildUpon()
.encodedQuery("filter[self]=true")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuCurrentUserResult>()
.data[0]
.id
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<KitsuCurrentUserResult>()
.data[0]
.id
}
}

View file

@ -26,21 +26,19 @@ class KomgaApi(private val client: OkHttpClient) {
withIOContext {
try {
val track =
with(json) {
if (url.contains(READLIST_API)) {
client.newCall(GET(url))
.awaitSuccess()
.parseAs<ReadListDto>()
.toTrack()
} else {
client.newCall(GET(url))
.awaitSuccess()
.parseAs<SeriesDto>()
.toTrack()
}
if (url.contains(READLIST_API)) {
client.newCall(GET(url))
.awaitSuccess()
.parseAs<ReadListDto>()
.toTrack()
} else {
client.newCall(GET(url))
.awaitSuccess()
.parseAs<SeriesDto>()
.toTrack()
}
val progress = with(json) {
val progress =
client
.newCall(
GET(
@ -59,7 +57,6 @@ class KomgaApi(private val client: OkHttpClient) {
it.parseAs<ReadProgressDto>().toV2()
}
}
}
track.apply {
cover_url = "$url/thumbnail"
tracking_url = url

View file

@ -37,15 +37,13 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
val listItem =
with(json) {
authClient.newCall(
GET(
url = "$BASE_URL/v1/lists/series/${track.media_id}",
),
)
.awaitSuccess()
.parseAs<MUListItem>()
}
authClient.newCall(
GET(
url = "$BASE_URL/v1/lists/series/${track.media_id}",
),
)
.awaitSuccess()
.parseAs<MUListItem>()
val rating = getSeriesRating(track)
@ -104,15 +102,13 @@ class MangaUpdatesApi(
private suspend fun getSeriesRating(track: Track): MURating? {
return try {
with(json) {
authClient.newCall(
GET(
url = "$BASE_URL/v1/series/${track.media_id}/rating",
),
)
.awaitSuccess()
.parseAs<MURating>()
}
authClient.newCall(
GET(
url = "$BASE_URL/v1/series/${track.media_id}/rating",
),
)
.awaitSuccess()
.parseAs<MURating>()
} catch (e: Exception) {
null
}
@ -151,18 +147,16 @@ class MangaUpdatesApi(
},
)
}
return with(json) {
client.newCall(
POST(
url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MUSearchResult>()
.results
.map { it.record }
}
return client.newCall(
POST(
url = "$BASE_URL/v1/series/search",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MUSearchResult>()
.results
.map { it.record }
}
suspend fun authenticate(username: String, password: String): MUContext? {
@ -170,17 +164,15 @@ class MangaUpdatesApi(
put("username", username)
put("password", password)
}
return with(json) {
client.newCall(
PUT(
url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MULoginResponse>()
.context
}
return client.newCall(
PUT(
url = "$BASE_URL/v1/account/login",
body = body.toString().toRequestBody(CONTENT_TYPE),
),
)
.awaitSuccess()
.parseAs<MULoginResponse>()
.context
}
companion object {

View file

@ -45,11 +45,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code")
.build()
with(json) {
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess()
.parseAs()
}
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess()
.parseAs()
}
}
@ -59,12 +57,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url("$BASE_API_URL/users/@me")
.get()
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALUser>()
.name
}
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALUser>()
.name
}
}
@ -75,15 +71,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALSearchResult>()
.data
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.filter { !it.publishing_type.contains("novel") }
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALSearchResult>()
.data
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.filter { !it.publishing_type.contains("novel") }
}
}
@ -93,24 +87,22 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALManga>()
.let {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
media_id = it.id
title = it.title
summary = it.synopsis
total_chapters = it.numChapters
cover_url = it.covers.large
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")
start_date = it.startDate ?: ""
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALManga>()
.let {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
media_id = it.id
title = it.title
summary = it.synopsis
total_chapters = it.numChapters
cover_url = (it.covers?.large ?: it.covers?.medium).orEmpty()
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")
start_date = it.startDate ?: ""
}
}
}
}
}
@ -132,12 +124,10 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build())
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALListItemStatus>()
.let { parseMangaItem(it, track) }
}
authClient.newCall(request)
.awaitSuccess()
.parseAs<MALListItemStatus>()
.let { parseMangaItem(it, track) }
}
}
@ -147,15 +137,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build()
with(json) {
authClient.newCall(GET(uri.toString()))
.awaitSuccess()
.parseAs<MALListItem>()
.let { item ->
track.total_chapters = item.numChapters
item.myListStatus?.let { parseMangaItem(it, track) }
}
}
authClient.newCall(GET(uri.toString()))
.awaitSuccess()
.parseAs<MALListItem>()
.let { item ->
track.total_chapters = item.numChapters
item.myListStatus?.let { parseMangaItem(it, track) }
}
}
}
@ -190,11 +178,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(urlBuilder.build().toString())
.get()
.build()
with(json) {
authClient.newCall(request)
.awaitSuccess()
.parseAs()
}
authClient.newCall(request)
.awaitSuccess()
.parseAs()
}
}

View file

@ -34,7 +34,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request
val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
.header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
// .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
.build()
return chain.proceed(authRequest)
@ -66,7 +66,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
return runCatching {
if (response.isSuccessful) {
with(json) { response.parseAs<MALOAuth>() }
response.parseAs<MALOAuth>()
} else {
response.close()
null

View file

@ -12,7 +12,7 @@ data class MALManga(
val numChapters: Long,
val mean: Double = -1.0,
@SerialName("main_picture")
val covers: MALMangaCovers,
val covers: MALMangaCovers?,
val status: String,
@SerialName("media_type")
val mediaType: String,
@ -22,5 +22,6 @@ data class MALManga(
@Serializable
data class MALMangaCovers(
val large: String = "",
val large: String?,
val medium: String,
)

View file

@ -74,12 +74,10 @@ class ShikimoriApi(
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<List<SMManga>>()
.map { it.toTrack(trackId) }
}
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<List<SMManga>>()
.map { it.toTrack(trackId) }
}
}
@ -102,51 +100,44 @@ class ShikimoriApi(
val urlMangas = "$API_URL/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build()
val manga = with(json) {
val manga =
authClient.newCall(GET(urlMangas.toString()))
.awaitSuccess()
.parseAs<SMManga>()
}
val url = "$API_URL/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.execute()
.parseAs<List<SMUserListEntry>>()
.let { entries ->
if (entries.size > 1) {
throw Exception("Too manga manga in response")
}
entries
.map { it.toTrack(trackId, manga) }
.firstOrNull()
authClient.newCall(GET(url.toString()))
.execute()
.parseAs<List<SMUserListEntry>>()
.let { entries ->
if (entries.size > 1) {
throw Exception("Too manga manga in response")
}
}
entries
.map { it.toTrack(trackId, manga) }
.firstOrNull()
}
}
}
suspend fun getCurrentUser(): Int {
return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess()
.parseAs<SMUser>()
.id
}
authClient.newCall(GET("$API_URL/users/whoami"))
.awaitSuccess()
.parseAs<SMUser>()
.id
}
}
suspend fun accessToken(code: String): SMOAuth {
return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
client.newCall(accessTokenRequest(code))
.awaitSuccess()
.parseAs()
}
}

View file

@ -52,9 +52,7 @@ class TachideskApi {
trackUrl
}
val manga = with(json) {
client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>()
}
val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>()
TrackSearch.create(TrackManager.SUWAYOMI).apply {
title = manga.title
@ -74,9 +72,7 @@ class TachideskApi {
suspend fun updateProgress(track: Track): Track {
val url = track.tracking_url
val chapters = with(json) {
client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>()
}
val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>()
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
client.newCall(

View file

@ -11,12 +11,12 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.models.Version
import java.util.*
import java.util.concurrent.*
class AppUpdateChecker(
private val json: Json = Injekt.get(),
@ -31,48 +31,46 @@ class AppUpdateChecker(
}
return withIOContext {
val result = with(json) {
if (preferences.checkForBetas().get()) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
.await()
.parseAs<List<GithubRelease>>()
.let { githubReleases ->
val releases =
githubReleases.take(10).filter { isNewVersion(it.version) }
// Check if any of the latest versions are newer than the current version
val release = releases
.maxWithOrNull { r1, r2 ->
when {
r1.version == r2.version -> 0
isNewVersion(r2.version, r1.version) -> -1
else -> 1
}
val result = if (preferences.checkForBetas().get()) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases"))
.await()
.parseAs<List<GithubRelease>>()
.let { githubReleases ->
val releases =
githubReleases.take(10).filter { isNewVersion(it.version) }
// Check if any of the latest versions are newer than the current version
val release = releases
.maxWithOrNull { r1, r2 ->
when {
r1.version == r2.version -> 0
isNewVersion(r2.version, r1.version) -> -1
else -> 1
}
preferences.lastAppCheck().set(Date().time)
if (release != null) {
AppUpdateResult.NewUpdate(release)
} else {
AppUpdateResult.NoNewUpdate
}
}
} else {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.await()
.parseAs<GithubRelease>()
.let {
preferences.lastAppCheck().set(Date().time)
preferences.lastAppCheck().set(Date().time)
// Check if latest version is newer than the current version
if (isNewVersion(it.version)) {
AppUpdateResult.NewUpdate(it)
} else {
AppUpdateResult.NoNewUpdate
}
if (release != null) {
AppUpdateResult.NewUpdate(release)
} else {
AppUpdateResult.NoNewUpdate
}
}
}
} else {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.await()
.parseAs<GithubRelease>()
.let {
preferences.lastAppCheck().set(Date().time)
// Check if latest version is newer than the current version
if (isNewVersion(it.version)) {
AppUpdateResult.NewUpdate(it)
} else {
AppUpdateResult.NoNewUpdate
}
}
}
if (doExtrasAfterNewUpdate && result is AppUpdateResult.NewUpdate) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&

View file

@ -7,6 +7,7 @@ import android.os.Parcelable
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.LoadResult
@ -17,6 +18,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
@ -27,8 +31,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.BasePreferences
import yokai.domain.extension.interactor.TrustExtension
import java.util.*
import java.util.concurrent.*
/**
* The manager of extensions installed as another apk which extend the available sources. It handles

View file

@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -24,7 +23,6 @@ import yokai.domain.extension.repo.model.ExtensionRepo
internal class ExtensionApi {
private val json: Json by injectLazy()
private val networkService: NetworkHelper by injectLazy()
private val getExtensionRepo: GetExtensionRepo by injectLazy()
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
@ -47,11 +45,9 @@ internal class ExtensionApi {
.newCall(GET("$repoBaseUrl/index.min.json"))
.awaitSuccess()
with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl)
}
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl)
} catch (e: Throwable) {
Logger.e(e) { "Failed to get extensions from $repoBaseUrl" }
emptyList()

View file

@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.extension.installer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
import uy.kohesive.injekt.injectLazy
abstract class Installer(
internal val context: Context,
// TODO: Remove finishedQueue
internal val finishedQueue: (Installer) -> Unit,
) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
abstract var ready: Boolean
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
}
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
data class Entry(
val downloadId: Long,
val pkgName: String,
val uri: Uri,
)
}

View file

@ -1,21 +1,16 @@
package eu.kanade.tachiyomi.extension
package eu.kanade.tachiyomi.extension.installer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Process
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -23,38 +18,21 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuRemoteProcess
import rikka.sui.Sui
import uy.kohesive.injekt.injectLazy
import java.io.BufferedReader
import java.io.InputStream
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import yokai.i18n.MR
import yokai.util.lang.getString
class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
class ShizukuInstaller(
context: Context,
finishedQueue: (Installer) -> Unit,
) : Installer(context, finishedQueue) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
Logger.d { "Shizuku was killed prematurely" }
finishedQueue(this)
}
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
@ -69,13 +47,13 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
}
}
var ready = false
override var ready = false
private val newProcess: Method
init {
Shizuku.addBinderDeadListener(shizukuDeadListener)
require(Shizuku.pingBinder() && (context.isPackageInstalled(shizukuPkgName) || Sui.isSui())) {
require(Shizuku.pingBinder() && context.isShizukuInstalled) {
finishedQueue(this)
context.getString(MR.strings.ext_installer_shizuku_stopped)
}
@ -91,9 +69,8 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
newProcess.isAccessible = true
}
@Suppress("BlockingMethodInNonBlockingContext")
fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
override fun processEntry(entry: Entry) {
super.processEntry(entry)
ioScope.launch {
var sessionId: String? = null
try {
@ -131,85 +108,14 @@ class ShizukuInstaller(private val context: Context, val finishedQueue: (Shizuku
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeAt(0)
processEntry(nextEntry)
}
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
// Don't cancel if entry is already started installing
fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun getActiveEntry(): Entry? = waitingInstall.get()
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun onDestroy() {
override fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
ioScope.cancel()
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
super.onDestroy()
}
private fun exec(command: String, stdin: InputStream? = null): ShellResult {

View file

@ -14,7 +14,7 @@ import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -47,7 +48,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import java.io.File
/**
* The installer which installs, updates and uninstalls the extensions.

View file

@ -12,8 +12,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.storage.fillMetadata
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.extension
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
@ -33,7 +31,8 @@ import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.archiveReader
import yokai.core.archive.util.archiveReader
import yokai.core.archive.util.epubReader
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.copyFromComicInfo
@ -42,6 +41,7 @@ import yokai.domain.chapter.services.ChapterRecognition
import yokai.domain.source.SourcePreferences
import yokai.domain.storage.StorageManager
import yokai.i18n.MR
import yokai.util.fillMetadata
import yokai.util.lang.getString
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
@ -410,7 +410,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
}
is Format.Epub -> {
EpubFile(format.file.archiveReader(context)).use { epub ->
format.file.epubReader(context).use { epub ->
val entry = epub.getImagesFromPages().firstOrNull()
entry?.let { updateCover(manga, epub.getInputStream(it)!!, context) }
@ -433,7 +433,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
true
}
is Format.Epub -> {
EpubFile(format.file.archiveReader(context)).use { epub ->
format.file.epubReader(context).use { epub ->
epub.fillMetadata(chapter, manga)
}
true

View file

@ -1,20 +1,13 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.Cubari
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.english.KireiCake
import eu.kanade.tachiyomi.source.online.english.MangaPlus
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -24,7 +17,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap
import yokai.i18n.MR
import yokai.util.lang.getString
class SourceManager(
private val context: Context,
@ -40,28 +34,8 @@ class SourceManager(
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
private val delegatedSources = listOf(
DelegatedSource(
"reader.kireicake.com",
5509224355268673176,
KireiCake(),
),
DelegatedSource(
"mangadex.org",
2499283573021220255,
MangaDex(),
),
DelegatedSource(
"mangaplus.shueisha.co.jp",
1998944621602463790,
MangaPlus(),
),
DelegatedSource(
"cubari.moe",
6338219619148105941,
Cubari(),
),
).associateBy { it.sourceId }
// FIXME: Delegated source, unused at the moment, J2K only delegate deep links
private val delegatedSources = emptyList<DelegatedSource>().associateBy { it.sourceId }
init {
scope.launch {
@ -71,8 +45,8 @@ class SourceManager(
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it
delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
// registerStubSource(it)
//delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
//registerStubSource(it)
}
}
sourcesMapFlow.value = mutableMap

View file

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.source.online
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.create
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
abstract class DelegatedHttpSource {
var delegate: HttpSource? = null
abstract val domainName: String
protected val getChapter: GetChapter by injectLazy()
protected val getManga: GetManga by injectLazy()
protected val network: NetworkHelper by injectLazy()
abstract fun canOpenUrl(uri: Uri): Boolean
abstract fun chapterUrl(uri: Uri): String?
open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull()
abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>?
protected open suspend fun getMangaInfo(url: String): Manga? {
val id = delegate?.id ?: return null
val manga = Manga.create(url, "", id)
val networkManga = delegate?.getMangaDetails(manga.copy()) ?: return null
val newManga = MangaImpl().apply {
this.url = url
title = try { networkManga.title } catch (e: Exception) { "" }
source = id
}
newManga.copyFrom(networkManga)
return newManga
}
suspend fun getChapters(url: String): List<SChapter>? {
val id = delegate?.id ?: return null
val manga = Manga.create(url, "", id)
return delegate?.getChapterList(manga)
}
}

View file

@ -1,22 +1,31 @@
package eu.kanade.tachiyomi.source.online.all
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class Cubari : DelegatedHttpSource() {
class Cubari(delegate: HttpSource) :
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
private val getChapter: GetChapter = Injekt.get()
override val lang = "all"
override val domainName: String = "cubari"
override fun canOpenUrl(uri: Uri): Boolean = true
@ -24,24 +33,24 @@ class Cubari : DelegatedHttpSource() {
override fun pageNumber(uri: Uri): Int? = uri.pathSegments.getOrNull(4)?.toIntOrNull()
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
val cubariType = uri.pathSegments.getOrNull(1)?.lowercase(Locale.ROOT) ?: return null
val cubariPath = uri.pathSegments.getOrNull(2) ?: return null
val chapterNumber = uri.pathSegments.getOrNull(3)?.replace("-", ".")?.toFloatOrNull() ?: return null
val mangaUrl = "/read/$cubariType/$cubariPath"
return withContext(Dispatchers.IO) {
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
}
val deferredChapters = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!)?.let { manga ->
getManga.awaitByUrlAndSource(mangaUrl, delegate.id)?.let { manga ->
val chapters = getChapter.awaitAll(manga, false)
val chapter = findChapter(chapters, cubariType, chapterNumber)
if (chapter != null) {
return@async chapters
}
}
getChapters(mangaUrl)
getChapterListByUrl(mangaUrl)
}
val manga = deferredManga.await()
val chapters = deferredChapters.await()
@ -50,11 +59,7 @@ class Cubari : DelegatedHttpSource() {
?: error(
context.getString(MR.strings.chapter_not_found),
)
if (manga != null) {
Triple(trueChapter, manga, chapters.orEmpty())
} else {
null
}
Triple(trueChapter, manga, chapters)
}
}

View file

@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.source.online.all
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -20,10 +20,15 @@ import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class MangaDex : DelegatedHttpSource() {
class MangaDex(delegate: HttpSource) : DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
override val lang: String = "all"
override val domainName: String = "mangadex"
@ -42,13 +47,13 @@ class MangaDex : DelegatedHttpSource() {
return uri.pathSegments.getOrNull(2)?.toIntOrNull()
}
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
val url = chapterUrl(uri) ?: return null
val request =
GET("https:///api.mangadex.org/v2$url", delegate!!.headers, CacheControl.FORCE_NETWORK)
val response = network.client.newCall(request).await()
if (response.code != 200) throw Exception("HTTP error ${response.code}")
val body = response.body.string().orEmpty()
val body = response.body.string()
if (body.isEmpty()) {
throw Exception("Null Response")
}
@ -56,28 +61,19 @@ class MangaDex : DelegatedHttpSource() {
val jsonObject = Json.decodeFromString<MangaDexChapterData>(body)
val dataObject = jsonObject.data ?: throw Exception("Chapter not found")
val mangaId = dataObject.mangaId ?: throw Exception("No manga associated with chapter")
val langCode = getRealLangCode(dataObject.language ?: "en").uppercase(Locale.getDefault())
// Use the correct MangaDex source based on the language code, or the api will not return
// the correct chapter list
delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" }
?: error("Source not found")
val mangaUrl = "/manga/$mangaId/"
return withContext(Dispatchers.IO) {
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
}
val deferredChapters = async { getChapters(mangaUrl) }
val deferredChapters = async { getChapterListByUrl(mangaUrl) }
val manga = deferredManga.await()
val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters?.find { it.url == "/api$url" }?.toChapter() ?: error(
val trueChapter = chapters.find { it.url == "/api$url" }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found),
)
if (manga != null) {
Triple(trueChapter, manga, chapters.orEmpty())
} else {
null
}
Triple(trueChapter, manga, chapters)
}
}

View file

@ -1,110 +0,0 @@
package eu.kanade.tachiyomi.source.online.english
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.i18n.MR
import yokai.util.lang.getString
open class FoolSlide(override val domainName: String, private val urlModifier: String = "") :
DelegatedHttpSource
() {
override fun canOpenUrl(uri: Uri): Boolean = true
override fun chapterUrl(uri: Uri): String? {
val offset = if (urlModifier.isEmpty()) 0 else 1
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null
val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null
val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString()
return "$urlModifier/read/" + listOfNotNull(
mangaName,
lang,
volume,
chapterNumber,
subChapterNumber,
).joinToString("/") + "/"
}
override fun pageNumber(uri: Uri): Int? {
val count = uri.pathSegments.count()
if (count > 2 && uri.pathSegments[count - 2] == "page") {
return super.pageNumber(uri)
}
return null
}
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
val offset = if (urlModifier.isEmpty()) 0 else 1
val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()
if (subChapterNumber != null) {
chapterNumber += ".$subChapterNumber"
}
return withContext(Dispatchers.IO) {
val mangaUrl = "$urlModifier/series/$mangaName/"
val sourceId = delegate?.id ?: return@withContext null
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, sourceId) ?: getManga(mangaUrl)
}
val chapterUrl = chapterUrl(uri)
val deferredChapters = async { getChapters(mangaUrl) }
val manga = deferredManga.await()
val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found),
)
if (manga != null) Triple(trueChapter, manga, chapters) else null
}
}
open suspend fun getManga(url: String): Manga? {
val request = GET("${delegate!!.baseUrl}$url")
val document = network.client.newCall(allowAdult(request)).await().asJsoup()
val mangaDetailsInfoSelector = "div.info"
val infoElement = document.select(mangaDetailsInfoSelector).first()?.text() ?: return null
return MangaImpl().apply {
this.url = url
source = delegate?.id ?: -1
title = infoElement.substringAfter("Title:").substringBefore("Author:").trim()
author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim()
artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim()
description = infoElement.substringAfter("Synopsis:").trim()
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim()
}
}
/**
* Transform a GET request into a POST request that automatically authorizes all adult content
*/
private fun allowAdult(request: Request) = allowAdult(request.url.toString())
private fun allowAdult(url: String): Request {
return POST(
url,
body = FormBody.Builder()
.add("adult", "true")
.build(),
)
}
}

View file

@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.source.online.english
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.lang.capitalizeWords
class KireiCake : FoolSlide("kireicake") {
override suspend fun getManga(url: String): Manga? {
val request = GET("${delegate!!.baseUrl}$url")
val document = network.client.newCall(request).await().asJsoup()
val mangaDetailsInfoSelector = "div.info"
return MangaImpl().apply {
this.url = url
source = delegate?.id ?: -1
title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first()
?.ownText()?.substringAfter(":")?.trim()
?: url.split("/").last().replace("_", " " + "").capitalizeWords()
description =
document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first()
?.ownText()?.substringAfter(":")
thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")
}
}
}

View file

@ -1,24 +1,31 @@
package eu.kanade.tachiyomi.source.online.english
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.manga.interactor.GetManga
import yokai.i18n.MR
import yokai.util.lang.getString
class MangaPlus : DelegatedHttpSource() {
class MangaPlus(delegate: HttpSource) :
DelegatedHttpSource(delegate) {
private val getManga: GetManga = Injekt.get()
override val lang: String get() = delegate.lang
override val domainName: String = "jumpg-webapi.tokyo-cdn"
private val titleIdRegex =
@ -34,11 +41,11 @@ class MangaPlus : DelegatedHttpSource() {
override fun pageNumber(uri: Uri): Int? = null
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<Chapter, Manga, List<SChapter>>? {
override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple<SChapter, SManga, List<SChapter>>? {
val url = chapterUrl(uri) ?: return null
val request = GET(
chapterUrlTemplate.replace("##", uri.pathSegments[1]),
delegate!!.headers,
delegate.headers,
CacheControl.FORCE_NETWORK,
)
return withContext(Dispatchers.IO) {
@ -53,26 +60,22 @@ class MangaPlus : DelegatedHttpSource() {
val trimmedTitle = title.substring(0, title.length - 1)
val mangaUrl = "#/titles/$titleId"
val deferredManga = async {
getManga.awaitByUrlAndSource(mangaUrl, delegate?.id!!) ?: getMangaInfo(mangaUrl)
getManga.awaitByUrlAndSource(mangaUrl, delegate.id) ?: getMangaDetailsByUrl(mangaUrl)
}
val deferredChapters = async { getChapters(mangaUrl) }
val deferredChapters = async { getChapterListByUrl(mangaUrl) }
val manga = deferredManga.await()
val chapters = deferredChapters.await()
val context = Injekt.get<PreferencesHelper>().context
val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error(
val trueChapter = chapters.find { it.url == url }?.toChapter() ?: error(
context.getString(MR.strings.chapter_not_found),
)
if (manga != null) {
Triple(
trueChapter,
manga.apply {
this.title = trimmedTitle
},
chapters,
)
} else {
null
}
Triple(
trueChapter,
manga.apply {
this.title = trimmedTitle
},
chapters,
)
}
}
}

View file

@ -5,19 +5,26 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import yokai.domain.DialogHostState
import yokai.presentation.theme.YokaiTheme
abstract class BaseComposeController(bundle: Bundle? = null) :
BaseController(bundle) {
override val shouldHideLegacyAppBar = true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
hideLegacyAppBar()
setAppBarVisibility()
return ComposeView(container.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@ -25,8 +32,14 @@ abstract class BaseComposeController(bundle: Bundle? = null) :
)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val dialogHostState = remember { DialogHostState() }
YokaiTheme {
ScreenContent()
CompositionLocalProvider(
LocalDialogHostState provides dialogHostState,
LocalBackPress provides router::handleBack,
) {
ScreenContent()
}
}
}
}

View file

@ -25,6 +25,8 @@ import kotlinx.coroutines.cancel
abstract class BaseController(bundle: Bundle? = null) :
Controller(bundle), BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface {
abstract val shouldHideLegacyAppBar: Boolean
lateinit var viewScope: CoroutineScope
var isDragging = false
@ -58,6 +60,10 @@ abstract class BaseController(bundle: Bundle? = null) :
open fun onViewCreated(view: View) { }
internal fun setAppBarVisibility() {
if (shouldHideLegacyAppBar) hideLegacyAppBar() else showLegacyAppBar()
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter && !isControllerVisible) {
view?.alpha = 0f

View file

@ -20,11 +20,13 @@ import eu.kanade.tachiyomi.util.view.isControllerVisible
abstract class BaseLegacyController<VB : ViewBinding>(bundle: Bundle? = null) :
BaseController(bundle) {
override val shouldHideLegacyAppBar = false
lateinit var binding: VB
val isBindingInitialized get() = this::binding.isInitialized
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
showLegacyAppBar()
setAppBarVisibility()
binding = createBinding(inflater)
binding.root.backgroundColor = binding.root.context.getResourceColor(R.attr.background)
return binding.root

View file

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.base.presenter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Presenter that mimic [cafe.adriel.voyager.core.model.StateScreenModel] for easier migration.
* Temporary class while we're migrating to Compose.
*/
abstract class StateCoroutinePresenter<S, C>(initialState: S) : BaseCoroutinePresenter<C>() {
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
val state: StateFlow<S> = mutableState.asStateFlow()
}

View file

@ -5,6 +5,8 @@ import android.content.Context
import android.util.AttributeSet
import android.view.MenuItem
import android.widget.LinearLayout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileDownloadOff
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.isInvisible
import androidx.core.view.updateLayoutParams
@ -212,7 +214,7 @@ class DownloadBottomSheet @JvmOverloads constructor(
setBottomSheet()
if (presenter.downloadQueueState.value.isEmpty()) {
binding.emptyView.show(
R.drawable.ic_download_off_24dp,
Icons.Filled.FileDownloadOff,
MR.strings.nothing_is_downloading,
)
} else {

View file

@ -13,15 +13,15 @@ import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext
import java.util.*
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
import java.util.*
import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.history.interactor.GetHistory
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
/**
* Adapter storing a list of manga in a certain category.
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun indexOf(manga: Manga): Int {
return currentItems.indexOfFirst {
if (it is LibraryItem) {
it.manga.id == manga.id
if (it is LibraryMangaItem) {
it.manga.manga.id == manga.id
} else {
false
}
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun allIndexOf(manga: Manga): List<Int> {
return currentItems.mapIndexedNotNull { index, it ->
if (it is LibraryItem && it.manga.id == manga.id) {
if (it is LibraryMangaItem && it.manga.manga.id == manga.id) {
index
} else {
null
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} else {
val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) {
val catId = mangas.firstOrNull()?.let { it.header?.catId ?: it.manga.category }
val catId = (mangas.firstOrNull() as? LibraryMangaItem)?.let { it.header?.catId ?: it.manga.category }
val blankItem = catId?.let { controller.presenter.blankItem(it) }
updateDataSet(blankItem ?: emptyList())
} else {
@ -173,6 +173,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
isLongPressDragEnabled = libraryListener?.canDrag() == true && s.isNullOrBlank()
setItemsPerCategoryMap()
notifyDataSetChanged()
}
private fun getFirstLetter(name: String): String {
@ -202,18 +203,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
vibrateOnCategoryChange(item.category.name)
item.category.name
}
is LibraryItem -> {
val text = if (item.manga.isBlank()) {
return item.header?.category?.name.orEmpty()
} else {
is LibraryPlaceholderItem -> {
item.header?.category?.name.orEmpty()
}
is LibraryMangaItem -> {
val text =
when (getSort(position)) {
LibrarySort.DragAndDrop -> {
if (item.header.category.isDynamic && item.manga.id != null) {
if (item.header.category.isDynamic && item.manga.manga.id != null) {
// FIXME: Don't do blocking
val category = runBlocking { getCategories.awaitByMangaId(item.manga.id!!) }.firstOrNull()?.name
val category = runBlocking { getCategories.awaitByMangaId(item.manga.manga.id!!) }.firstOrNull()?.name
category ?: context.getString(MR.strings.default_value)
} else {
val title = item.manga.title
val title = item.manga.manga.title
if (preferences.removeArticles().get()) {
title.removeArticles().chop(15)
} else {
@ -222,14 +224,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.DateFetched -> {
val id = item.manga.id ?: return ""
val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getChapter.awaitAll(id, false) }
val last = history.maxOfOrNull { it.date_fetch }
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
}
LibrarySort.LastRead -> {
val id = item.manga.id ?: return ""
val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getHistory.awaitAllByMangaId(id) }
val last = history.maxOfOrNull { it.last_read }
@ -256,21 +258,23 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.LatestChapter -> {
context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update)
context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update)
}
LibrarySort.DateAdded -> {
context.timeSpanFromNow(MR.strings.added_, item.manga.date_added)
context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added)
}
LibrarySort.Title -> {
val title = if (preferences.removeArticles().get()) {
item.manga.title.removeArticles()
item.manga.manga.title.removeArticles()
} else {
item.manga.title
item.manga.manga.title
}
getFirstLetter(title)
}
LibrarySort.Random -> {
context.getString(MR.strings.random)
}
}
}
if (!isSingleCategory) {
vibrateOnCategoryChange(item.header?.category?.name.orEmpty())
}

View file

@ -28,6 +28,8 @@ import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HeartBroken
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.ViewCompat
@ -449,7 +451,7 @@ open class LibraryController(
private fun setActiveCategory() {
val currentCategory = presenter.categories.indexOfFirst {
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategory == it.id
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategoryId == it.id
}
if (currentCategory > -1) {
binding.categoryRecycler.setCategories(currentCategory)
@ -519,14 +521,13 @@ open class LibraryController(
}
private fun openRandomManga(global: Boolean) {
val items = if (global) {
presenter.allLibraryItems
} else {
adapter.currentItems
}.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) }
val items =
if (global) { presenter.currentLibraryItems } else { adapter.currentItems }
.filterIsInstance<LibraryMangaItem>()
.filter { !it.manga.manga.initialized || it.manga.unread > 0 }
if (items.isNotEmpty()) {
val item = items.random() as LibraryItem
openManga(item.manga)
val item = items.random() as LibraryMangaItem
openManga(item.manga.manga)
}
}
@ -557,7 +558,7 @@ open class LibraryController(
}
presenter.groupType = item
shouldScrollToTop = true
presenter.getLibrary()
presenter.updateLibrary()
true
}.show()
}
@ -660,7 +661,7 @@ open class LibraryController(
createActionModeIfNeeded()
}
if (presenter.libraryItems.isNotEmpty() && !isSubClass) {
if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) {
presenter.restoreLibrary()
if (justStarted) {
val activityBinding = activityBinding ?: return
@ -704,7 +705,7 @@ open class LibraryController(
if (!LibraryUpdateJob.isRunning(context)) {
when {
!presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> {
presenter.findCurrentCategory()?.let {
presenter.currentCategory?.let {
updateLibrary(it)
}
}
@ -902,7 +903,7 @@ open class LibraryController(
}
} else {
val newOffset =
presenter.categories.indexOfFirst { presenter.currentCategory == it.id } +
presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } +
(if (next) 1 else -1)
if (if (!next) {
newOffset > -1
@ -1011,7 +1012,7 @@ open class LibraryController(
override fun getSpanSize(position: Int): Int {
if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount
val item = this@LibraryController.mAdapter?.getItem(position)
return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) {
return if (item is LibraryHeaderItem || item is SearchGlobalItem || item is LibraryPlaceholderItem) {
managerSpanCount
} else {
1
@ -1056,7 +1057,7 @@ open class LibraryController(
if (type.isEnter) {
binding.filterBottomSheet.filterBottomSheet.isVisible = true
if (type == ControllerChangeType.POP_ENTER) {
presenter.getLibrary()
presenter.updateLibrary()
isPoppingIn = true
}
binding.recyclerCover.isClickable = false
@ -1095,7 +1096,7 @@ open class LibraryController(
if (!isBindingInitialized) return
updateFilterSheetY()
if (observeLater) {
presenter.getLibrary()
presenter.updateLibrary()
}
}
@ -1135,7 +1136,7 @@ open class LibraryController(
binding.emptyView.hide()
} else {
binding.emptyView.show(
R.drawable.ic_heart_off_24dp,
Icons.Filled.HeartBroken,
if (hasActiveFilters) {
MR.strings.no_matches_for_filters
} else {
@ -1374,7 +1375,7 @@ open class LibraryController(
setActiveCategory()
return
}
val headerPosition = adapter.indexOf(pos)
val headerPosition = mAdapter?.indexOf(pos) ?: return
if (headerPosition > -1) {
val activityBinding = activityBinding ?: return
val index = adapter.headerItems.indexOf(adapter.getItem(headerPosition))
@ -1408,7 +1409,7 @@ open class LibraryController(
private fun onRefresh() {
showCategories(false)
presenter.getLibrary()
presenter.updateLibrary()
destroyActionModeIfNeeded()
}
@ -1432,14 +1433,14 @@ open class LibraryController(
val isShowAllCategoriesSet = preferences.showAllCategories().get()
if (!query.isNullOrBlank() && this.query.isBlank() && !isShowAllCategoriesSet) {
presenter.forceShowAllCategories = preferences.showAllCategoriesWhenSearchingSingleCategory().get()
presenter.getLibrary()
presenter.updateLibrary()
} else if (query.isNullOrBlank() && this.query.isNotBlank() && !isShowAllCategoriesSet) {
if (!isSubClass) {
preferences.showAllCategoriesWhenSearchingSingleCategory()
.set(presenter.forceShowAllCategories)
}
presenter.forceShowAllCategories = false
presenter.getLibrary()
presenter.updateLibrary()
}
if (query != this.query && !query.isNullOrBlank()) {
@ -1457,7 +1458,7 @@ open class LibraryController(
adapter.removeAllScrollableHeaders()
}
adapter.setFilter(query)
if (presenter.allLibraryItems.isEmpty()) return true
if (presenter.currentLibraryItems.isEmpty()) return true
viewScope.launchUI {
adapter.performFilterAsync()
}
@ -1476,7 +1477,6 @@ open class LibraryController(
}
private fun setSelection(manga: Manga, selected: Boolean) {
if (manga.isBlank()) return
val currentMode = adapter.mode
if (selected) {
if (selectedMangas.add(manga)) {
@ -1526,7 +1526,7 @@ open class LibraryController(
toggleSelection(position)
return
}
val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return
val manga = (adapter.getItem(position) as? LibraryMangaItem)?.manga?.manga ?: return
val activity = activity ?: return
val chapter = presenter.getFirstUnread(manga) ?: return
activity.apply {
@ -1542,9 +1542,8 @@ open class LibraryController(
}
private fun toggleSelection(position: Int) {
val item = adapter.getItem(position) as? LibraryItem ?: return
if (item.manga.isBlank()) return
setSelection(item.manga, !adapter.isSelected(position))
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
setSelection(item.manga.manga, !adapter.isSelected(position))
invalidateActionMode()
}
@ -1560,14 +1559,14 @@ open class LibraryController(
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(view: View?, position: Int): Boolean {
val item = adapter.getItem(position) as? LibraryItem ?: return false
val item = adapter.getItem(position) as? LibraryMangaItem ?: return false
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
snack?.dismiss()
lastClickPosition = position
toggleSelection(position)
false
} else {
openManga(item.manga)
openManga(item.manga.manga)
false
}
}
@ -1589,10 +1588,10 @@ open class LibraryController(
*/
override fun onItemLongClick(position: Int) {
val item = adapter.getItem(position)
if (item !is LibraryItem) return
if (item !is LibraryMangaItem) return
snack?.dismiss()
if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) {
snack = view?.snack(item.manga.title) {
snack = view?.snack(item.manga.manga.title) {
anchorView = activityBinding?.bottomNav
view.elevation = 15f.dpToPx
}
@ -1641,14 +1640,14 @@ open class LibraryController(
if (mangaId == null) {
adapter.getHeaderPositions().forEach { adapter.notifyItemChanged(it) }
} else {
presenter.updateManga()
presenter.updateLibrary()
}
}
private fun setSelection(position: Int, selected: Boolean = true) {
val item = adapter.getItem(position) as? LibraryItem ?: return
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
setSelection(item.manga, selected)
setSelection(item.manga.manga, selected)
invalidateActionMode()
}
@ -1672,7 +1671,7 @@ open class LibraryController(
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
val item = adapter.getItem(fromPosition) as? LibraryMangaItem ?: return false
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
if (toPosition < 1) return false
return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (
@ -1687,18 +1686,18 @@ open class LibraryController(
lastItem = null
isDragging = false
binding.swipeRefresh.isEnabled = true
if (adapter.selectedItemCount > 0) {
if (mAdapter == null || adapter.selectedItemCount > 0) {
lastItemPosition = null
return
}
destroyActionModeIfNeeded()
// if nothing moved
if (lastItemPosition == null) return
val item = adapter.getItem(position) as? LibraryItem ?: return
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem
val libraryItems = getSectionItems(adapter.getSectionHeader(position), item)
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
if (newHeader?.category?.id == item.manga.category) {
presenter.rearrangeCategory(item.manga.category, mangaIds)
} else {
@ -1820,7 +1819,7 @@ open class LibraryController(
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
if (!category.isDynamic) {
ManageCategoryDialog(category) {
presenter.getLibrary()
presenter.updateLibrary()
}.showDialog(router)
}
}
@ -1830,8 +1829,8 @@ open class LibraryController(
if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) {
val item = adapter.findCategoryHeader(catId) ?: return
val libraryItems = adapter.getSectionItems(item)
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
presenter.rearrangeCategory(catId, mangaIds)
} else {
presenter.sortCategory(catId, sortBy)
@ -1913,7 +1912,7 @@ open class LibraryController(
isGone = true
setOnClickListener {
presenter.forceShowAllCategories = !presenter.forceShowAllCategories
presenter.getLibrary()
presenter.updateLibrary()
isSelected = presenter.forceShowAllCategories
}
val pad = 12.dpToPx
@ -2193,7 +2192,7 @@ open class LibraryController(
val activity = activity ?: return
viewScope.launchIO {
selectedMangas.toList().moveCategories(activity) {
presenter.getLibrary()
presenter.updateLibrary()
destroyActionModeIfNeeded()
}
}

View file

@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.backgroundColor
import eu.kanade.tachiyomi.util.view.setCards
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.loadManga
/**
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -64,23 +64,24 @@ class LibraryGridHolder(
* @param item the manga item to bind.
*/
override fun onSetValues(item: LibraryItem) {
if (item !is LibraryMangaItem) throw IllegalStateException("Only LibraryMangaItem can use grid holder")
// Update the title and subtitle of the manga.
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition"
binding.constraintLayout.isVisible = !item.manga.isBlank()
binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.title
val mangaColor = item.manga.dominantCoverColors
binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.manga.title
val mangaColor = item.manga.manga.dominantCoverColors
binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background)
binding.behindTitle.setTextColor(
mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground),
)
val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
binding.subtitle.text = authorArtist.highlightText(item.filter, color)
@ -101,7 +102,7 @@ class LibraryGridHolder(
// Update the cover.
binding.coverThumbnail.dispose()
setCover(item.manga)
setCover(item.manga.manga)
}
override fun toggleActivation() {

View file

@ -32,14 +32,17 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.util.view.setText
import eu.kanade.tachiyomi.util.view.text
import kotlin.random.Random
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.library.LibraryPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
BaseFlexibleViewHolder(view, adapter, true) {
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val binding = LibraryCategoryHeaderItemBinding.bind(view)
val progressDrawableStart = CircularProgressDrawable(itemView.context)
val progressDrawableEnd = CircularProgressDrawable(itemView.context)
@ -183,7 +186,17 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
binding.categoryTitle.text = categoryName +
if (adapter.showNumber) {
" (${adapter.itemsPerCategory[item.catId]})"
val filteredCount = adapter.currentItems.count {
it is LibraryMangaItem && it.header?.catId == item.catId
}
val totalCount = adapter.itemsPerCategory[item.catId] ?: 0
val searchText = adapter.getFilter(String::class.java)
var countText = if (searchText.isNullOrBlank()) {
" ($totalCount)"
} else {
" ($filteredCount/$totalCount)"
}
countText
} else { "" }
if (category.sourceId != null) {
val icon = adapter.sourceManager.get(category.sourceId!!)?.icon()
@ -198,7 +211,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
val isAscending = category.isAscending()
val sortingMode = category.sortingMode()
val sortDrawable = getSortRes(sortingMode, isAscending, R.drawable.ic_sort_24dp)
val sortDrawable = getSortRes(sortingMode, isAscending, category.isDynamic, false)
binding.categorySort.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, sortDrawable, 0)
binding.categorySort.setText(category.sortRes())
@ -263,7 +276,7 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
val cat = category ?: return
adapter.controller?.activity?.let { activity ->
val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) }
val sortingMode = cat.sortingMode(true)
val sortingMode = cat.sortingMode() ?: if (!cat.isDynamic) LibrarySort.DragAndDrop else null
val sheet = MaterialMenuSheet(
activity,
items,
@ -272,69 +285,68 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) :
) { sheet, item ->
onCatSortClicked(cat, item)
val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category
val isAscending = nCategory?.isAscending() ?: false
val drawableRes = getSortRes(item, isAscending)
sheet.setDrawable(item, drawableRes)
sheet.updateSortIcon(nCategory, LibrarySort.valueOf(item))
false
}
val isAscending = cat.isAscending()
val drawableRes = getSortRes(sortingMode, isAscending)
sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
sheet.updateSortIcon(cat, sortingMode)
sheet.show()
}
}
private fun MaterialMenuSheet.updateSortIcon(category: Category?, sortingMode: LibrarySort?) {
val isAscending = category?.isAscending() ?: false
val drawableRes = getSortRes(sortingMode, isAscending, category?.isDynamic ?: false, true)
this.setDrawable(sortingMode?.mainValue ?: -1, drawableRes)
}
private fun getSortRes(
sortMode: LibrarySort?,
isAscending: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
isDynamic: Boolean,
onSelection: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_sort_24dp,
@DrawableRes defaultSelectedDrawableRes: Int = R.drawable.ic_check_24dp,
): Int {
sortMode ?: return defaultDrawableRes
return when (sortMode) {
LibrarySort.DragAndDrop -> defaultDrawableRes
else -> {
if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
R.drawable.ic_arrow_upward_24dp
}
}
}
}
sortMode ?: return if (onSelection) defaultSelectedDrawableRes else defaultDrawableRes
private fun getSortRes(
sortingMode: Int?,
isAscending: Boolean,
@DrawableRes defaultDrawableRes: Int = R.drawable.ic_check_24dp,
): Int {
sortingMode ?: return defaultDrawableRes
return when (val sortMode = LibrarySort.valueOf(sortingMode)) {
LibrarySort.DragAndDrop -> defaultDrawableRes
else -> {
if (if (sortMode?.hasInvertedSort == true) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
R.drawable.ic_arrow_upward_24dp
}
if (sortMode.isDirectional) {
return if (if (sortMode.hasInvertedSort) !isAscending else isAscending) {
R.drawable.ic_arrow_downward_24dp
} else {
R.drawable.ic_arrow_upward_24dp
}
}
if (onSelection) {
return when(sortMode) {
LibrarySort.DragAndDrop -> R.drawable.ic_check_24dp
LibrarySort.Random -> R.drawable.ic_refresh_24dp
else -> defaultSelectedDrawableRes
}
}
return sortMode.iconRes(isDynamic)
}
private fun onCatSortClicked(category: Category, menuId: Int?) {
val modType = if (menuId == null) {
val (mode, modType) = if (menuId == null) {
val sortingMode = category.sortingMode() ?: LibrarySort.Title
if (category.isAscending()) {
sortingMode.categoryValueDescending
} else {
sortingMode.categoryValue
}
sortingMode to
if (sortingMode != LibrarySort.Random && category.isAscending()) {
sortingMode.categoryValueDescending
} else {
sortingMode.categoryValue
}
} else {
val sortingMode = LibrarySort.valueOf(menuId) ?: LibrarySort.Title
if (sortingMode != LibrarySort.DragAndDrop && sortingMode == category.sortingMode()) {
onCatSortClicked(category, null)
return
}
sortingMode.categoryValue
sortingMode to sortingMode.categoryValue
}
if (mode == LibrarySort.Random) {
libraryPreferences.randomSortSeed().set(Random.nextInt())
}
adapter.libraryListener?.sortCategory(category.id!!, modType)
}

View file

@ -5,9 +5,6 @@ import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.system.getResourceColor
@ -17,9 +14,7 @@ import eu.kanade.tachiyomi.util.view.setCards
* Generic class used to hold the displayed data of a manga in the library.
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to the single tap and long tap events.
*/
abstract class LibraryHolder(
view: View,
val adapter: LibraryCategoryAdapter,
@ -43,7 +38,7 @@ abstract class LibraryHolder(
*/
abstract fun onSetValues(item: LibraryItem)
fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) {
fun setUnreadBadge(badge: LibraryBadge, item: LibraryMangaItem) {
val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters
badge.setUnreadDownload(
when {
@ -54,7 +49,7 @@ abstract class LibraryHolder(
},
when {
item.downloadCount == -1 -> -1
item.manga.isLocal() -> -2
item.manga.manga.isLocal() -> -2
else -> item.downloadCount
},
showTotal,
@ -63,7 +58,7 @@ abstract class LibraryHolder(
)
}
fun setReadingButton(item: LibraryItem) {
fun setReadingButton(item: LibraryMangaItem) {
itemView.findViewById<View>(R.id.play_layout)?.isVisible =
item.manga.unread > 0 && !item.hideReadingButton
}
@ -80,8 +75,8 @@ abstract class LibraryHolder(
override fun onLongClick(view: View?): Boolean {
return if (adapter.isLongPressDragEnabled) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga
if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga
if (manga != null && !isDraggable) {
adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition)
toggleActivation()
true

View file

@ -1,205 +1,48 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
class LibraryItem(
val manga: LibraryManga,
abstract class LibraryItem(
header: LibraryHeaderItem,
private val context: Context?,
internal val context: Context?,
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
var filter = ""
private val sourceManager: SourceManager by injectLazy()
internal val sourceManager: SourceManager by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val uniformSize: Boolean
internal val uniformSize: Boolean
get() = uiPreferences.uniformGrid().get()
private val libraryLayout: Int
internal val libraryLayout: Int
get() = preferences.libraryLayout().get()
val hideReadingButton: Boolean
get() = preferences.hideStartReadingButton().get()
override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
} else {
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "15:22"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga, parent)
}
gridHolder
}
} else {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
}
@CallSuper
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this)
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position))
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
layoutParams?.isFullSpan = manga.isBlank()
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return !manga.isBlank() && header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return !manga.isBlank()
}
override fun isSelectable(): Boolean {
return !manga.isBlank()
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.isBlank() && manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryItem) {
return manga.id == other.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.id!!.hashCode() + header!!.hashCode()
(holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem
}
companion object {

View file

@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.util.lang.highlightText
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.setCards
import yokai.i18n.MR
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.loadManga
import yokai.util.lang.getString
/**
@ -39,22 +39,25 @@ class LibraryListHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.title.isVisible = true
binding.constraintLayout.minHeight = 56.dpToPx
if (item.manga.isBlank()) {
if (item is LibraryPlaceholderItem) {
binding.constraintLayout.minHeight = 0
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
}
if (item.manga.status == -1) {
binding.title.text = null
binding.title.isVisible = false
} else {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
when (item.type) {
is LibraryPlaceholderItem.Type.Blank -> {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.type.mangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
}
is LibraryPlaceholderItem.Type.Hidden -> {
binding.title.text = null
binding.title.isVisible = false
}
}
binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.card.isVisible = false
@ -63,6 +66,9 @@ class LibraryListHolder(
binding.subtitle.isVisible = false
return
}
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = 52.dpToPx
}
@ -71,16 +77,16 @@ class LibraryListHolder(
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Update the binding.title of the manga.
binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
setUnreadBadge(binding.unreadDownloadBadge.badgeView, item)
val authorArtist =
if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
@ -95,7 +101,7 @@ class LibraryListHolder(
// Update the cover.
binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga)
binding.coverThumbnail.loadManga(item.manga.manga)
}
override fun onActionStateChanged(position: Int, actionState: Int) {

View file

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
class LibraryMangaItem(
val manga: LibraryManga,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val listHolder by lazy { LibraryListHolder(view, adapter as LibraryCategoryAdapter) }
val parent = adapter.recyclerView
if (parent !is AutofitRecyclerView) return listHolder
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST) { return listHolder }
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "2:3"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga.manga, parent)
}
return gridHolder
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga.manga, adapter.recyclerView as? AutofitRecyclerView)
}
super.bindViewHolder(adapter, holder, position, payloads)
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return true
}
override fun isSelectable(): Boolean {
return true
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.manga.source).name }
return manga.manga.title.contains(constraint, true) ||
(manga.manga.author?.contains(constraint, true) ?: false) ||
(manga.manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryMangaItem) {
return manga.manga.id == other.manga.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.manga.id.hashCode() + header!!.hashCode()
}
}

View file

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Placeholder item to indicate if the category is hidden or empty/filtered out.
*/
class LibraryPlaceholderItem (
val category: Int,
val type: Type,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
override fun getLayoutRes(): Int = R.layout.manga_list_item
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
return LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
override fun filter(constraint: String): Boolean {
filter = constraint
if (type !is Type.Hidden || type.title.isBlank()) return constraint.isEmpty()
return type.title.contains(constraint, true)
}
override fun equals(other: Any?): Boolean {
if (other is LibraryPlaceholderItem) {
return category == other.category
}
return false
}
override fun hashCode(): Int {
return 31 * Long.MIN_VALUE.hashCode() + header!!.hashCode()
}
sealed class Type {
data class Hidden(val title: String, val hiddenItems: List<LibraryMangaItem>) : Type()
data class Blank(val mangaCount: Int) : Type()
}
companion object {
fun hidden(category: Int, header: LibraryHeaderItem, context: Context?, title: String, hiddenItems: List<LibraryMangaItem>) =
LibraryPlaceholderItem(category, Type.Hidden(title, hiddenItems), header, context)
fun blank(category: Int, header: LibraryHeaderItem, context: Context?, mangaCount: Int = 0) =
LibraryPlaceholderItem(category, Type.Blank(mangaCount), header, context)
}
}

View file

@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.ui.library
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import yokai.i18n.MR
enum class LibrarySort(
val mainValue: Int,
@ -33,7 +30,11 @@ enum class LibrarySort(
MR.strings.category,
R.drawable.ic_label_outline_24dp,
),
Random(
8,
MR.strings.random,
R.drawable.ic_shuffle_24dp,
),
;
val categoryValue: Char
@ -50,6 +51,7 @@ enum class LibrarySort(
LatestChapter -> "LATEST_CHAPTER"
DateFetched -> "CHAPTER_FETCH_DATE"
DateAdded -> "DATE_ADDED"
Random -> "RANDOM"
else -> "ALPHABETICAL"
}
return "$type,ASCENDING"
@ -63,6 +65,9 @@ enum class LibrarySort(
val hasInvertedSort: Boolean
get() = this in listOf(LastRead, DateAdded, LatestChapter, DateFetched)
val isDirectional: Boolean
get() = this !in listOf(DragAndDrop, Random)
fun menuSheetItem(isDynamic: Boolean): MaterialMenuSheet.MenuSheetItem {
return MaterialMenuSheet.MenuSheetItem(
mainValue,
@ -85,6 +90,7 @@ enum class LibrarySort(
"LATEST_CHAPTER" -> LatestChapter
"CHAPTER_FETCH_DATE" -> DateFetched
"DATE_ADDED" -> DateAdded
"RANDOM" -> Random
else -> Title
}
} catch (e: Exception) {

View file

@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.ui.library.compose
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.core.view.isGone
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
import eu.kanade.tachiyomi.ui.main.BottomSheetController
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import java.util.Locale
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.presentation.library.LibraryContent
import yokai.presentation.theme.YokaiTheme
import yokai.util.lang.getString
class LibraryComposeController(
bundle: Bundle? = null,
val uiPreferences: UiPreferences = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
) : BaseCoroutineController<LibraryControllerBinding, LibraryComposePresenter>(bundle) ,
BottomSheetController,
RootSearchInterface,
FloatingSearchInterface {
override fun getTitle(): String? {
return view?.context?.getString(MR.strings.library)
}
override fun getSearchTitle(): String? {
val searchSuggestion by lazy { preferences.librarySearchSuggestion().get() }
return searchTitle(
if (preferences.showLibrarySearchSuggestions().get() && searchSuggestion.isNotBlank()) {
"\"$searchSuggestion\""
} else {
view?.context?.getString(MR.strings.your_library)?.lowercase(Locale.ROOT)
},
)
}
override val presenter = LibraryComposePresenter()
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.composeView.isVisible = true
binding.swipeRefresh.isGone = true
binding.fastScroller.isGone = true
binding.composeView.setContent {
YokaiTheme {
ScreenContent()
}
}
}
@Composable
fun ScreenContent() {
val nestedScrollInterop = rememberNestedScrollInteropConnection()
val state by presenter.state.collectAsState()
LibraryContent(
modifier = Modifier.nestedScroll(nestedScrollInterop),
items = (0..50).map { LibraryItem.Blank(it) },
columns = 3,
)
}
override fun showSheet() {
}
override fun hideSheet() {
}
override fun toggleSheet() {
}
}

View file

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.library.compose
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.presenter.StateCoroutinePresenter
import eu.kanade.tachiyomi.ui.library.models.LibraryItem
typealias LibraryMap = Map<Category, List<LibraryItem>>
class LibraryComposePresenter :
StateCoroutinePresenter<LibraryComposePresenter.State, LibraryComposeController>(State()) {
data class State(
var isLoading: Boolean = true,
var library: LibraryMap = emptyMap()
)
}

View file

@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.library.display
import android.content.Context
import android.util.AttributeSet
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.databinding.LibraryCategoryLayoutBinding
import eu.kanade.tachiyomi.util.bindToPreference
import eu.kanade.tachiyomi.util.lang.withSubtitle
import eu.kanade.tachiyomi.util.system.toInt
import eu.kanade.tachiyomi.widget.BaseLibraryDisplayView
import kotlin.math.min
import yokai.i18n.MR
import yokai.util.lang.getString
class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
BaseLibraryDisplayView<LibraryCategoryLayoutBinding>(context, attrs) {
@ -20,7 +18,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun initGeneralPreferences() {
with(binding) {
showAll.bindToPreference(preferences.showAllCategories()) {
controller?.presenter?.getLibrary()
controller?.presenter?.updateLibrary()
binding.categoryShow.isEnabled = it
}
categoryShow.isEnabled = showAll.isChecked
@ -30,7 +28,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
dynamicToBottom.text = context.getString(MR.strings.move_dynamic_to_bottom)
.withSubtitle(context, MR.strings.when_grouping_by_sources_tags)
dynamicToBottom.bindToPreference(preferences.collapsedDynamicAtBottom()) {
controller?.presenter?.getLibrary()
controller?.presenter?.updateLibrary()
}
showEmptyCatsFiltering.bindToPreference(preferences.showEmptyCategoriesWhileFiltering()) {
controller?.presenter?.requestFilterUpdate()

View file

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.ui.library.LibraryMangaItem
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.launchUI
@ -36,6 +37,8 @@ import eu.kanade.tachiyomi.util.view.isCollapsed
import eu.kanade.tachiyomi.util.view.isExpanded
import eu.kanade.tachiyomi.util.view.isHidden
import eu.kanade.tachiyomi.util.view.setText
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
@ -48,8 +51,6 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.i18n.MR
import yokai.util.lang.getString
import kotlin.math.max
import kotlin.math.roundToInt
class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs),
@ -368,11 +369,12 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
suspend fun checkForManhwa(sourceManager: SourceManager) {
if (checked) return
withIOContext {
val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext
val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext
checked = true
var types = mutableSetOf<StringResource>()
libraryManga.forEach {
when (it.manga.seriesType(sourceManager = sourceManager)) {
if (it !is LibraryMangaItem) return@forEach
when (it.manga.manga.seriesType(sourceManager = sourceManager)) {
Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
Manga.TYPE_COMIC -> types.add(MR.strings.comic)
@ -637,7 +639,7 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
SeriesType('m', MR.strings.series_type),
Bookmarked('b', MR.strings.bookmarked),
Tracked('t', MR.strings.tracking),
ContentType('s', MR.strings.content_type);
ContentType('s', MR.strings.content_type)
;
companion object {

View file

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.library.models
import eu.kanade.tachiyomi.data.database.models.LibraryManga
sealed interface LibraryItem {
data class Blank(val mangaCount: Int = 0) : LibraryItem
data class Hidden(val title: String, val hiddenItems: List<LibraryItem>) : LibraryItem
data class Manga(
val libraryManga: LibraryManga,
val isLocal: Boolean = false,
val downloadCount: Long = -1,
val unreadCount: Long = -1,
val language: String = "",
) : LibraryItem
}

View file

@ -86,6 +86,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.BaseLegacyController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.compose.LibraryComposeController
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.more.AboutController
import eu.kanade.tachiyomi.ui.more.OverflowDialog
@ -117,6 +118,7 @@ import eu.kanade.tachiyomi.util.system.prepareSideNavContext
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission
import eu.kanade.tachiyomi.util.system.withUIContext
import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface
import eu.kanade.tachiyomi.util.view.backgroundColor
import eu.kanade.tachiyomi.util.view.blurBehindWindow
@ -136,12 +138,10 @@ import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToLong
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
import yokai.core.migration.Migrator
import yokai.domain.base.BasePreferences
@ -198,6 +198,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
dimenW to dimenH
}
@Deprecated("Create contract directly from Composable")
private val requestNotificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
@ -538,7 +539,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
if (currentRoot?.tag()?.toIntOrNull() != id) {
setRoot(
when (id) {
R.id.nav_library -> LibraryController()
R.id.nav_library -> if (basePreferences.composeLibrary().get()) LibraryComposeController() else LibraryController()
R.id.nav_recents -> RecentsController()
else -> BrowseController()
},
@ -1001,7 +1002,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
}
private fun checkForAppUpdates() {
if (isUpdaterEnabled) {
if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) {
lifecycleScope.launchIO {
try {
val result = updateChecker.checkForUpdate(this@MainActivity)
@ -1011,7 +1012,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
val isBeta = result.release.preRelease == true
// Create confirmation window
withContext(Dispatchers.Main) {
withUIContext {
showNotificationPermissionPrompt()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router)
@ -1037,6 +1038,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
@SuppressLint("MissingSuperCall")
override fun onNewIntent(intent: Intent) {
splashState.ready = true
if (!handleIntentAction(intent)) {
super.onNewIntent(intent)
}
@ -1053,13 +1055,13 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
}
when (intent.action) {
SHORTCUT_LIBRARY -> nav.selectedItemId = R.id.nav_library
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, SHORTCUT_RECENTS -> {
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ, Constants.SHORTCUT_RECENTS -> {
if (nav.selectedItemId != R.id.nav_recents) {
nav.selectedItemId = R.id.nav_recents
} else {
router.popToRoot()
}
if (intent.action == SHORTCUT_RECENTS) return true
if (intent.action == Constants.SHORTCUT_RECENTS) return true
nav.post {
val controller =
router.backstack.firstOrNull()?.controller as? RecentsController
@ -1092,7 +1094,11 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
SHORTCUT_UPDATE_NOTES -> {
val extras = intent.extras ?: return false
if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library
if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) {
if (
router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController &&
// FIXME: Show Compose version of NewUpdateDialog for AboutController
router.backstack.lastOrNull()?.controller !is AboutController
) {
AboutController.NewUpdateDialogController(extras).showDialog(router)
}
}
@ -1122,7 +1128,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
else -> return false
}
splashState.ready = true
return true
}
@ -1609,20 +1614,12 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
private const val SWIPE_THRESHOLD = 100
private const val SWIPE_VELOCITY_THRESHOLD = 100
const val MAIN_ACTIVITY = Constants.MAIN_ACTIVITY
// Shortcut actions
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_RECENTS = Constants.SHORTCUT_RECENTS
const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
const val SHORTCUT_RECENTLY_READ = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
const val SHORTCUT_BROWSE = "eu.kanade.tachiyomi.SHOW_BROWSE"
const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_MANGA = Constants.SHORTCUT_MANGA
@Deprecated("Use the one from Constants object instead")
const val SHORTCUT_MANGA_BACK = Constants.SHORTCUT_MANGA_BACK
const val SHORTCUT_UPDATE_NOTES = "eu.kanade.tachiyomi.SHOW_UPDATE_NOTES"
const val SHORTCUT_SOURCE = "eu.kanade.tachiyomi.SHOW_SOURCE"
const val SHORTCUT_READER_SETTINGS = "eu.kanade.tachiyomi.READER_SETTINGS"

View file

@ -97,7 +97,7 @@ class SearchActivity : MainActivity() {
}
private fun intentShouldGoBack() =
intent.action in listOf(SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE)
intent.action in listOf(Constants.SHORTCUT_MANGA, SHORTCUT_READER_SETTINGS, SHORTCUT_BROWSE)
override fun syncActivityViewWithController(
to: Controller?,

View file

@ -43,8 +43,8 @@ import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.models.cover
import yokai.i18n.MR
import yokai.presentation.core.util.coil.asTarget
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.asTarget
import yokai.util.coil.loadManga
import yokai.util.lang.getString
import android.R as AR

View file

@ -806,6 +806,8 @@ class MangaDetailsController :
}
private fun getHeader(): MangaHeaderHolder? {
if (!isBindingInitialized) return null
return if (isTablet) {
binding.tabletRecycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder
} else {

View file

@ -47,7 +47,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.view.resetStrokeColor
import yokai.i18n.MR
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.loadManga
import yokai.util.lang.getString
import android.R as AR

View file

@ -18,6 +18,8 @@ import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SearchOff
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri
import androidx.core.view.WindowInsetsCompat.Type.systemBars
@ -31,7 +33,6 @@ import com.google.android.material.datepicker.MaterialDatePicker
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.fastadapter.listeners.addClickListener
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -317,7 +318,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
if (results.isEmpty()) {
setMiddleTrackView(binding.searchEmptyView.id)
binding.searchEmptyView.show(
R.drawable.ic_search_off_24dp,
Icons.Filled.SearchOff,
MR.strings.no_results_found,
)
} else {
@ -338,7 +339,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
binding.trackSearchRecycler.isVisible = false
searchItemAdapter.clear()
binding.searchEmptyView.show(
R.drawable.ic_search_off_24dp,
Icons.Filled.SearchOff,
error.message ?: "",
)
}

View file

@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.setCards
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.loadManga
class MangaHolder(
view: View,

View file

@ -27,7 +27,7 @@ import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.models.cover
import yokai.i18n.MR
import yokai.presentation.core.util.coil.loadManga
import yokai.util.coil.loadManga
import yokai.util.lang.getString
class MigrationProcessHolder(

View file

@ -1,175 +1,41 @@
package eu.kanade.tachiyomi.ui.more
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.BuildConfig
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.CrossfadeTransition
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.add
import eu.kanade.tachiyomi.ui.setting.onClick
import eu.kanade.tachiyomi.ui.setting.preference
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
import eu.kanade.tachiyomi.ui.setting.titleMRes
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setNegativeButton
import eu.kanade.tachiyomi.util.view.setPositiveButton
import eu.kanade.tachiyomi.util.view.setTitle
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import yokai.i18n.MR
import yokai.util.lang.getString
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import yokai.presentation.settings.screen.about.AboutScreen
import android.R as AR
class AboutController : SettingsLegacyController() {
class AboutController : BaseComposeController() {
/**
* Checks for new releases
*/
private val updateChecker by lazy { AppUpdateChecker() }
private val dateFormat: DateFormat by lazy {
preferences.dateFormat()
}
private val isUpdaterEnabled = BuildConfig.INCLUDE_UPDATER
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleMRes = MR.strings.about
preference {
key = "pref_whats_new"
titleMRes = MR.strings.whats_new_this_release
onClick {
val intent = Intent(
Intent.ACTION_VIEW,
if (BuildConfig.DEBUG) {
"https://github.com/null2264/yokai/commits/master"
} else {
RELEASE_URL
}.toUri(),
)
startActivity(intent)
}
}
if (isUpdaterEnabled) {
preference {
key = "pref_check_for_updates"
titleMRes = MR.strings.check_for_updates
onClick {
if (activity!!.isOnline()) {
checkVersion()
} else {
activity!!.toast(MR.strings.no_network_connection)
}
}
}
}
preference {
key = "pref_version"
titleMRes = MR.strings.version
summary = if (BuildConfig.DEBUG || BuildConfig.NIGHTLY) {
"r" + BuildConfig.COMMIT_COUNT
} else {
BuildConfig.VERSION_NAME
}
onClick {
activity?.let {
val deviceInfo = CrashLogUtil(it.localeContext).getDebugInfo()
val clipboard = it.getSystemService<ClipboardManager>()!!
val appInfo = it.getString(MR.strings.app_info)
clipboard.setPrimaryClip(ClipData.newPlainText(appInfo, deviceInfo))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
view?.snack(context.getString(MR.strings._copied_to_clipboard, appInfo))
}
}
}
}
preference {
key = "pref_build_time"
titleMRes = MR.strings.build_time
summary = getFormattedBuildTime(dateFormat)
}
preferenceCategory {
preference {
key = "pref_oss"
titleMRes = MR.strings.open_source_licenses
onClick {
router.pushController(AboutLicenseController().withFadeTransaction())
}
}
}
add(AboutLinksPreference(context))
}
/**
* Checks version and shows a user prompt if an update is available.
*/
private fun checkVersion() {
val activity = activity ?: return
activity.toast(MR.strings.searching_for_updates)
viewScope.launch {
val result = try {
updateChecker.checkForUpdate(activity, true)
} catch (error: Exception) {
withContext(Dispatchers.Main) {
activity.toast(error.message)
Logger.e(error) { "Couldn't check new update" }
}
}
when (result) {
is AppUpdateResult.NewUpdate -> {
val body = result.release.info
val url = result.release.downloadLink
val isBeta = result.release.preRelease == true
// Create confirmation window
withContext(Dispatchers.Main) {
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
NewUpdateDialogController(body, url, isBeta).showDialog(router)
}
}
is AppUpdateResult.NoNewUpdate -> {
withContext(Dispatchers.Main) {
activity.toast(MR.strings.no_new_updates_available)
}
}
}
}
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutScreen(),
content = {
CrossfadeTransition(navigator = it)
},
)
}
@Deprecated("Use [DialogHostState.showNewUpdateDialog] instead", ReplaceWith("DialogHostState.showNewUpdateDialog()"))
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
constructor(body: String, url: String, isBeta: Boolean?) : this(
@ -181,9 +47,7 @@ class AboutController : SettingsLegacyController() {
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val releaseBody = (args.getString(BODY_KEY) ?: "")
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
val info = activity!!.parseReleaseNotes(args.getString(BODY_KEY) ?: "")
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val isBeta = args.getBoolean(IS_BETA, false)
@ -220,19 +84,9 @@ class AboutController : SettingsLegacyController() {
const val IS_BETA = "NewUpdateDialogController.is_beta"
}
}
companion object {
fun getFormattedBuildTime(dateFormat: DateFormat): String {
try {
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault())
inputDf.timeZone = TimeZone.getTimeZone("UTC")
val buildTime =
inputDf.parse(BuildConfig.BUILD_TIME) ?: return BuildConfig.BUILD_TIME
return buildTime.toTimestampString(dateFormat)
} catch (e: ParseException) {
return BuildConfig.BUILD_TIME
}
}
}
}
fun Context.parseReleaseNotes(releaseNotes: String): Spanned {
val releaseBody = releaseNotes.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
return Markwon.create(this).toMarkdown(releaseBody)
}

View file

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.ui.more
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import soup.compose.material.motion.animation.materialSharedAxisZ
class AboutLicenseController : BaseComposeController() {
@Composable
override fun ScreenContent() {
Navigator(
screen = AboutLicenseScreen(),
content = {
CompositionLocalProvider(LocalBackPress provides router::handleBack) {
ScreenTransition(
navigator = it,
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
)
}
},
)
}
}

View file

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.ui.more
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.compatToolTipText
class AboutLinksPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) {
init {
layoutResource = R.layout.pref_about_links
isSelectable = false
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
/*
(holder.itemView as LinearLayout).apply {
checkHeightThen {
val childCount = (this.getChildAt(0) as ViewGroup).childCount
val childCount2 = (this.getChildAt(1) as ViewGroup).childCount
val fullCount = childCount + childCount2
orientation =
if (width >= (56 * fullCount).dpToPx) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
}
}
*/
holder.findViewById(R.id.btn_website).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://mihon.app") }
}
holder.findViewById(R.id.btn_discord).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://discord.gg/mihon") }
}
holder.findViewById(R.id.btn_github).apply {
compatToolTipText = (contentDescription.toString())
setOnClickListener { context.openInBrowser("https://github.com/null2264/yokai") }
}
}
}

View file

@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.util.system.roundToTwoDecimal
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlin.math.roundToInt
import yokai.i18n.MR
import yokai.util.lang.getString
import kotlin.math.roundToInt
import android.R as AR
class StatsController : BaseLegacyController<StatsControllerBinding>() {
@ -61,7 +61,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
private fun handleGeneralStats() {
val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it) }
val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it.manga) }
scoresList = getScoresList(mangaTracks)
with(binding) {
viewDetailLayout.isVisible = mangaDistinct.isNotEmpty()
@ -76,8 +76,8 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString()
statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString()
statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString()
statsSourcesText.text = presenter.getSources().count().toString()
statsTrackersText.text = presenter.getLoggedTrackers().count().toString()
@ -105,7 +105,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
val pieEntries = ArrayList<PieEntry>()
val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) ->
val libraryCount = mangaDistinct.count { it.status == status }
val libraryCount = mangaDistinct.count { it.manga.status == status }
if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null
pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status)))
StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color)

View file

@ -65,19 +65,19 @@ class StatsPresenter(
val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt)
val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt)
val restrictions = prefs.libraryUpdateMangaRestriction().get()
return libraryMangas.groupBy { it.id }
return libraryMangas.groupBy { it.manga.id }
.filterNot { it.value.any { manga -> manga.category in excludedCategories } }
.filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } }
.filterNot {
val manga = it.value.first()
(MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED) ||
(MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED) ||
(MANGA_HAS_UNREAD in restrictions && manga.unread != 0) ||
(MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead)
}
}
fun getDownloadCount(manga: LibraryManga): Int {
return downloadManager.getDownloadCount(manga)
return downloadManager.getDownloadCount(manga.manga)
}
fun get10PointScore(track: Track): Float? {

View file

@ -12,6 +12,8 @@ import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HeartBroken
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.ColorUtils
import androidx.core.util.Pair
@ -458,7 +460,10 @@ class StatsDetailsController :
with(binding ?: headerBinding) {
val hasNoData = currentStats.isNullOrEmpty() || currentStats.all { it.count == 0 }
if (hasNoData) {
this@StatsDetailsController.binding.noChartData.show(R.drawable.ic_heart_off_24dp, MR.strings.no_data_for_filters)
this@StatsDetailsController.binding.noChartData.show(
Icons.Filled.HeartBroken,
MR.strings.no_data_for_filters,
)
presenter.currentStats?.removeAll { it.count == 0 }
handleNoChartLayout()
this?.statsPieChart?.isVisible = false

View file

@ -153,7 +153,7 @@ class StatsDetailsPresenter(
private suspend fun setupSeriesType() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.seriesType() }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.seriesType() }
libraryFormat.forEach { (seriesType, mangaList) ->
currentStats?.add(
@ -173,7 +173,7 @@ class StatsDetailsPresenter(
private suspend fun setupStatus() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.status }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.status }
libraryFormat.forEach { (status, mangaList) ->
currentStats?.add(
@ -263,7 +263,7 @@ class StatsDetailsPresenter(
private suspend fun setupTrackers() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip()
.map { it to getTracks(it).ifEmpty { listOf(null) } }
.map { it to getTracks(it.manga).ifEmpty { listOf(null) } }
.flatMap { it.second.map { track -> it.first to track } }
val loggedServices = trackManager.services.filter { it.isLogged }
@ -292,7 +292,7 @@ class StatsDetailsPresenter(
private suspend fun setupSources() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.source }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.source }
libraryFormat.forEach { (sourceId, mangaList) ->
val source = sourceManager.getOrStub(sourceId)
@ -339,10 +339,10 @@ class StatsDetailsPresenter(
private suspend fun setupTags() {
currentStats = ArrayList()
val mangaFiltered = mangasDistinct.filterByChip()
val tags = mangaFiltered.flatMap { it.getTags() }.distinctBy { it.uppercase() }
val tags = mangaFiltered.flatMap { it.manga.getTags() }.distinctBy { it.uppercase() }
val libraryFormat = tags.map { tag ->
tag to mangaFiltered.filter {
it.getTags().any { mangaTag -> mangaTag.equals(tag, true) }
it.manga.getTags().any { mangaTag -> mangaTag.equals(tag, true) }
}
}
@ -433,7 +433,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
context.mapSeriesType(manga.seriesType()) in selectedSeriesType
context.mapSeriesType(manga.manga.seriesType()) in selectedSeriesType
}
}
}
@ -443,7 +443,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
context.mapStatus(manga.status) in selectedStatus
context.mapStatus(manga.manga.status) in selectedStatus
}
}
}
@ -463,7 +463,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
manga.source in selectedSource.map { it.id }
manga.manga.source in selectedSource.map { it.id }
}
}
}
@ -504,10 +504,10 @@ class StatsDetailsPresenter(
* Get language name of a manga
*/
private fun LibraryManga.getLanguage(): String {
val code = if (isLocal()) {
LocalSource.getMangaLang(this)
val code = if (manga.isLocal()) {
LocalSource.getMangaLang(this.manga)
} else {
sourceManager.get(source)?.lang
sourceManager.get(manga.source)?.lang
} ?: return context.getString(MR.strings.unknown)
return LocaleHelper.getLocalizedDisplayName(code)
}
@ -516,7 +516,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to two decimal of a list of manga
*/
private suspend fun List<LibraryManga>.getMeanScoreRounded(): Double? {
val mangaTracks = this.map { it to getTracks(it) }
val mangaTracks = this.map { it to getTracks(it.manga) }
val scoresList = mangaTracks.filter { it.second.isNotEmpty() }
.mapNotNull { it.second.getMeanScoreByTracker() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToTwoDecimal()
@ -526,7 +526,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to int of a single manga
*/
private suspend fun LibraryManga.getMeanScoreToInt(): Int? {
val mangaTracks = getTracks(this)
val mangaTracks = getTracks(this.manga)
val scoresList = mangaTracks.filter { it.score > 0 }
.mapNotNull { it.get10PointScore() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10)
@ -550,8 +550,8 @@ class StatsDetailsPresenter(
}
private suspend fun LibraryManga.getStartYear(): Int? {
if (getChapter.awaitAll(id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 }
if (getChapter.awaitAll(manga.id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 }
val date = chapters.minOfOrNull { it.last_read } ?: return null
val cal = Calendar.getInstance().apply { timeInMillis = date }
return if (date <= 0L) null else cal.get(Calendar.YEAR)
@ -564,7 +564,7 @@ class StatsDetailsPresenter(
}
private fun getEnabledSources(): List<Source> {
return mangasDistinct.mapNotNull { sourceManager.get(it.source) }
return mangasDistinct.mapNotNull { sourceManager.get(it.manga.source) }
.distinct().sortedBy { it.name }
}
@ -589,7 +589,7 @@ class StatsDetailsPresenter(
}
private suspend fun List<LibraryManga>.getReadDuration(): Long {
return sumOf { manga -> getHistory.awaitAllByMangaId(manga.id!!).sumOf { it.time_read } }
return sumOf { manga -> getHistory.awaitAllByMangaId(manga.manga.id!!).sumOf { it.time_read } }
}
/**

View file

@ -148,6 +148,14 @@ import eu.kanade.tachiyomi.util.view.setMessage
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.doOnEnd
import eu.kanade.tachiyomi.widget.doOnStart
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Collections
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -167,13 +175,6 @@ import yokai.domain.ui.settings.ReaderPreferences
import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import yokai.i18n.MR
import yokai.util.lang.getString
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import android.R as AR
/**
@ -512,7 +513,6 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
}
}
viewModel.onSaveInstanceState()
super.onSaveInstanceState(outState)
}
@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
override fun onPause() {
viewModel.saveCurrentChapterReadingProgress()
viewModel.flushReadTimer()
super.onPause()
}
override fun onResume() {
super.onResume()
viewModel.setReadStartTime()
viewModel.restartReadTimer()
}
fun reloadChapters(doublePages: Boolean, force: Boolean = false) {
@ -1655,7 +1655,7 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
}
private fun showSetCoverPrompt(page: ReaderPage) {
if (page.status != Page.State.READY) return
if (page.status !is Page.State.Ready) return
materialAlertDialog()
.setMessage(MR.strings.use_image_as_cover)

View file

@ -12,6 +12,7 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.create
import eu.kanade.tachiyomi.data.database.models.defaultReaderType
import eu.kanade.tachiyomi.data.database.models.orientationType
import eu.kanade.tachiyomi.data.database.models.readingModeType
@ -54,9 +55,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import eu.kanade.tachiyomi.util.system.withUIContext
import java.util.Date
import java.util.concurrent.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -68,6 +67,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -80,6 +80,7 @@ import yokai.domain.chapter.models.ChapterUpdate
import yokai.domain.download.DownloadPreferences
import yokai.domain.history.interactor.GetHistory
import yokai.domain.history.interactor.UpsertHistory
import yokai.domain.library.LibraryPreferences
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.InsertManga
import yokai.domain.manga.interactor.UpdateManga
@ -101,6 +102,7 @@ class ReaderViewModel(
private val chapterFilter: ChapterFilter = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) : ViewModel() {
private val getCategories: GetCategories by injectLazy()
private val getChapter: GetChapter by injectLazy()
@ -155,12 +157,15 @@ class ReaderViewModel(
private var finished = false
private var chapterToDownload: Download? = null
private val unfilteredChapterList by lazy {
val manga = manga!!
runBlocking { getChapter.awaitAll(manga, filterScanlators = false) }
}
private lateinit var chapterList: List<ReaderChapter>
private var chapterItems = emptyList<ReaderChapterItem>()
private var scope = CoroutineScope(Job() + Dispatchers.Default)
private var hasTrackers: Boolean = false
private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty()
@ -191,24 +196,12 @@ class ReaderViewModel(
val currentChapters = state.value.viewerChapters
if (currentChapters != null) {
currentChapters.unref()
saveReadingProgress(currentChapters.currChapter)
chapterToDownload?.let {
downloadManager.addDownloadsToStartOfQueue(listOf(it))
}
}
}
/**
* Called when the activity is saved and not changing configurations. It updates the database
* to persist the current progress of the active chapter.
*/
fun onSaveInstanceState() {
val currentChapter = getCurrentChapter() ?: return
viewModelScope.launchNonCancellableIO {
saveChapterProgress(currentChapter)
}
}
/**
* Whether this presenter is initialized yet.
*/
@ -307,6 +300,7 @@ class ReaderViewModel(
return delegatedSource.pageNumber(url)?.minus(1)
}
// FIXME: Unused at the moment, handles J2K's delegated deep link, refactor or remove later
suspend fun loadChapterURL(url: Uri) {
val host = url.host ?: return
val context = Injekt.get<Application>()
@ -314,9 +308,7 @@ class ReaderViewModel(
context.getString(MR.strings.source_not_installed),
)
val chapterUrl = delegatedSource.chapterUrl(url)
val sourceId = delegatedSource.delegate?.id ?: error(
context.getString(MR.strings.source_not_installed),
)
val sourceId = delegatedSource.delegate.id
if (chapterUrl != null) {
val dbChapter = getChapter.awaitAllByUrl(chapterUrl, false).find {
val source = getManga.awaitById(it.manga_id!!)?.source ?: return@find false
@ -334,7 +326,9 @@ class ReaderViewModel(
}
val info = delegatedSource.fetchMangaFromChapterUrl(url)
if (info != null) {
val (chapter, manga, chapters) = info
val (sChapter, sManga, chapters) = info
val manga = Manga.create(sManga.url, sManga.title, sourceId).apply { copyFrom(sManga) }
val chapter = Chapter.create().apply { copyFrom(sChapter) }
val id = insertManga.await(manga)
manga.id = id ?: manga.id
chapter.manga_id = manga.id
@ -373,12 +367,15 @@ class ReaderViewModel(
* Called when the user changed to the given [chapter] when changing pages from the viewer.
* It's used only to set this chapter as active.
*/
private suspend fun loadNewChapter(chapter: ReaderChapter) {
private fun loadNewChapter(chapter: ReaderChapter) {
val loader = loader ?: return
Logger.d { "Loading ${chapter.chapter.url}" }
viewModelScope.launchIO {
Logger.d { "Loading ${chapter.chapter.url}" }
flushReadTimer()
restartReadTimer()
withIOContext {
try {
loadChapter(loader, chapter)
} catch (e: Throwable) {
@ -509,28 +506,15 @@ class ReaderViewModel(
val selectedChapter = page.chapter
// Save last page read and mark as read if needed
selectedChapter.chapter.last_page_read = page.index
selectedChapter.chapter.pages_left =
(selectedChapter.pages?.size ?: page.index) - page.index
val shouldTrack = !preferences.incognitoMode().get() || hasTrackers
if (shouldTrack &&
// For double pages, check if the second to last page is doubled up
(
(selectedChapter.pages?.lastIndex == page.index && page.firstHalf != true) ||
(hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index)
)
) {
selectedChapter.chapter.read = true
updateTrackChapterAfterReading(selectedChapter)
deleteChapterIfNeeded(selectedChapter)
viewModelScope.launchNonCancellableIO {
saveChapterProgress(selectedChapter, page, hasExtraPage)
}
if (selectedChapter != currentChapters.currChapter) {
Logger.d { "Setting ${selectedChapter.chapter.url} as active" }
saveReadingProgress(currentChapters.currChapter)
setReadStartTime()
scope.launch { loadNewChapter(selectedChapter) }
loadNewChapter(selectedChapter)
}
val pages = page.chapter.pages ?: return
val inDownloadRange = page.number.toDouble() / pages.size > 0.2
if (inDownloadRange) {
@ -618,28 +602,28 @@ class ReaderViewModel(
}
}
/**
* Called when reader chapter is changed in reader or when activity is paused.
*/
private fun saveReadingProgress(readerChapter: ReaderChapter) {
viewModelScope.launchNonCancellableIO {
saveChapterProgress(readerChapter)
saveChapterHistory(readerChapter)
}
}
fun saveCurrentChapterReadingProgress() = getCurrentChapter()?.let { saveReadingProgress(it) }
/**
* Saves this [readerChapter]'s progress (last read page and whether it's read).
* If incognito mode isn't on or has at least 1 tracker
*/
private suspend fun saveChapterProgress(readerChapter: ReaderChapter) {
private suspend fun saveChapterProgress(readerChapter: ReaderChapter, page: ReaderPage, hasExtraPage: Boolean) {
readerChapter.requestedPage = readerChapter.chapter.last_page_read
getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter ->
readerChapter.chapter.bookmark = dbChapter.bookmark
}
if (!preferences.incognitoMode().get() || hasTrackers) {
val shouldTrack = !preferences.incognitoMode().get() || hasTrackers
if (shouldTrack && page.status !is Page.State.Error) {
readerChapter.chapter.last_page_read = page.index
readerChapter.chapter.pages_left = (readerChapter.pages?.size ?: page.index) - page.index
// For double pages, check if the second to last page is doubled up
if (
(readerChapter.pages?.lastIndex == page.index && page.firstHalf != true) ||
(hasExtraPage && readerChapter.pages?.lastIndex?.minus(1) == page.index)
) {
onChapterReadComplete(readerChapter)
}
updateChapter.await(
ChapterUpdate(
id = readerChapter.chapter.id!!,
@ -652,24 +636,56 @@ class ReaderViewModel(
}
}
private suspend fun onChapterReadComplete(readerChapter: ReaderChapter) {
readerChapter.chapter.read = true
updateTrackChapterAfterReading(readerChapter)
deleteChapterIfNeeded(readerChapter)
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
.contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING)
if (!markDuplicateAsRead) return
val duplicateUnreadChapters = unfilteredChapterList
.mapNotNull { chapter ->
if (
!chapter.read &&
chapter.isRecognizedNumber &&
chapter.chapter_number == readerChapter.chapter.chapter_number
) {
ChapterUpdate(id = chapter.id!!, read = true)
} else {
null
}
}
updateChapter.awaitAll(duplicateUnreadChapters)
}
fun restartReadTimer() {
chapterReadStartTime = Date().time
}
fun flushReadTimer() {
getCurrentChapter()?.let {
viewModelScope.launchNonCancellableIO {
saveChapterHistory(it)
}
}
}
/**
* Saves this [readerChapter] last read history.
*/
private suspend fun saveChapterHistory(readerChapter: ReaderChapter) {
if (!preferences.incognitoMode().get()) {
val readAt = Date().time
val sessionReadDuration = chapterReadStartTime?.let { readAt - it } ?: 0
val history = History.create(readerChapter.chapter).apply {
last_read = readAt
time_read = sessionReadDuration
}
upsertHistory.await(history)
chapterReadStartTime = null
}
}
if (preferences.incognitoMode().get()) return
fun setReadStartTime() {
chapterReadStartTime = Date().time
val endTime = Date().time
val sessionReadDuration = chapterReadStartTime?.let { endTime - it } ?: 0
val history = History.create(readerChapter.chapter).apply {
last_read = endTime
time_read = sessionReadDuration
}
upsertHistory.await(history)
chapterReadStartTime = null
}
/**
@ -844,7 +860,7 @@ class ReaderViewModel(
* There's also a notification to allow sharing the image somewhere else or deleting it.
*/
fun saveImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
if (page.status !is Page.State.Ready) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -874,9 +890,9 @@ class ReaderViewModel(
}
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
scope.launch {
if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status != Page.State.READY) return@launch
viewModelScope.launch {
if (firstPage.status !is Page.State.Ready) return@launch
if (secondPage.status !is Page.State.Ready) return@launch
val manga = manga ?: return@launch
val context = Injekt.get<Application>()
@ -910,7 +926,7 @@ class ReaderViewModel(
* image will be kept so it won't be taking lots of internal disk space.
*/
fun shareImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
if (page.status !is Page.State.Ready) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -923,9 +939,9 @@ class ReaderViewModel(
}
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
scope.launch {
if (firstPage.status != Page.State.READY) return@launch
if (secondPage.status != Page.State.READY) return@launch
viewModelScope.launch {
if (firstPage.status !is Page.State.Ready) return@launch
if (secondPage.status !is Page.State.Ready) return@launch
val manga = manga ?: return@launch
val context = Injekt.get<Application>()
@ -942,7 +958,7 @@ class ReaderViewModel(
* Sets the image of this [page] as cover and notifies the UI of the result.
*/
fun setAsCover(page: ReaderPage) {
if (page.status != Page.State.READY) return
if (page.status !is Page.State.Ready) return
val manga = manga ?: return
val stream = page.stream ?: return

View file

@ -11,6 +11,8 @@ import yokai.core.archive.ArchiveReader
*/
internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader() {
override val isLocal: Boolean = true
/**
* Recycles this loader and the open archive.
*/
@ -29,7 +31,7 @@ internal class ArchivePageLoader(private val reader: ArchiveReader) : PageLoader
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { reader.getInputStream(entry.name)!! }
status = Page.State.READY
status = Page.State.Ready
}
}
.toList()

View file

@ -10,7 +10,8 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.system.withIOContext
import yokai.core.archive.archiveReader
import yokai.core.archive.util.archiveReader
import yokai.core.archive.util.epubReader
import yokai.i18n.MR
import yokai.util.lang.getString
@ -82,7 +83,7 @@ class ChapterLoader(
when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalSource.Format.Archive -> ArchivePageLoader(format.file.archiveReader(context))
is LocalSource.Format.Epub -> EpubPageLoader(format.file.archiveReader(context))
is LocalSource.Format.Epub -> EpubPageLoader(format.file.epubReader(context))
}
}
else -> error(context.getString(MR.strings.source_not_installed))

View file

@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
*/
class DirectoryPageLoader(val file: UniFile) : PageLoader() {
override val isLocal: Boolean = true
/**
* Returns the pages found on this directory ordered with a natural comparator.
*/
@ -22,7 +24,7 @@ class DirectoryPageLoader(val file: UniFile) : PageLoader() {
val streamFn = { file.openInputStream() }
ReaderPage(i).apply {
stream = streamFn
status = Page.State.READY
status = Page.State.Ready
}
} ?: emptyList()
}

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import uy.kohesive.injekt.injectLazy
import yokai.core.archive.archiveReader
import yokai.core.archive.util.archiveReader
/**
* Loader used to load a chapter from the downloaded chapters.
@ -24,6 +24,8 @@ class DownloadPageLoader(
private val downloadProvider: DownloadProvider,
) : PageLoader() {
override val isLocal: Boolean = true
// Needed to open input streams
private val context: Application by injectLazy()
@ -58,7 +60,7 @@ class DownloadPageLoader(
ReaderPage(page.index, page.url, page.imageUrl, stream = {
context.contentResolver.openInputStream(page.uri ?: Uri.EMPTY)!!
},).apply {
status = Page.State.READY
status = Page.State.Ready
}
}
}

View file

@ -2,18 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile
import yokai.core.archive.ArchiveReader
import yokai.core.archive.EpubReader
/**
* Loader used to load a chapter from a .epub file.
*/
class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
class EpubPageLoader(private val epub: EpubReader) : PageLoader() {
/**
* The epub file.
*/
private val epub = EpubFile(reader)
override val isLocal: Boolean = true
/**
* Recycles this loader and the open zip.
@ -32,7 +28,7 @@ class EpubPageLoader(reader: ArchiveReader) : PageLoader() {
val streamFn = { epub.getInputStream(path)!! }
ReaderPage(i).apply {
stream = streamFn
status = Page.State.READY
status = Page.State.Ready
}
}
}

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