Compare commits

...

156 Commits

Author SHA1 Message Date
Roshan Varughese
0d4735630a Commas :< 2024-12-25 23:54:05 +13:00
Roshan Varughese
87f50551c8 Separate to new file 2024-12-25 23:50:30 +13:00
Roshan Varughese
b3c1a683e7 Removing Comments 2024-12-25 23:11:22 +13:00
Roshan Varughese
4e75adbeb5 i18n 2024-12-25 23:11:22 +13:00
Roshan Varughese
38fbcdfd0a Applying Suggestions 2024-12-25 23:11:22 +13:00
Roshan Varughese
4e0c042057 Fixing Refactor 2024-12-25 23:11:22 +13:00
Roshan Varughese
154811d10a Save as CSV 2024-12-25 23:11:22 +13:00
Roshan Varughese
7668021d1a Spotless 2024-12-25 23:11:22 +13:00
Roshan Varughese
1c0000011e Add Library List 2024-12-25 23:11:22 +13:00
AntsyLich
4a7fe44e0e
Use secrets.GITHUB_TOKEN for release 2024-12-24 21:34:15 +06:00
AntsyLich
c5655e8803
Revert "Revert "Add option to always use SSIV for image decoding""
This reverts commit 1909126921
2024-12-22 02:38:12 +06:00
Mend Renovate
d3973f4ad8
Update dependency gradle to v8.12 (#1605) 2024-12-22 02:14:44 +06:00
Mend Renovate
bb230fd6a7
Update dependency androidx.compose:compose-bom to v2024.12.01 (#1564) 2024-12-22 01:32:17 +06:00
Mend Renovate
e526fd44c6
Update paging.version to v3.3.5 (#1563) 2024-12-22 01:28:40 +06:00
Mend Renovate
f61f039a45
Update dependency androidx.viewpager:viewpager to v1.1.0 (#1571) 2024-12-22 01:27:28 +06:00
Mend Renovate
79eb02d8f0
Update dependency org.junit.jupiter:junit-jupiter to v5.11.4 (#1580) 2024-12-22 01:25:21 +06:00
Mend Renovate
814584d35b
Update voyager to v1.0.1 (#1595) 2024-12-22 01:24:43 +06:00
Mend Renovate
8751307301
Update dependency com.android.tools:desugar_jdk_libs to v2.1.4 (#1599) 2024-12-22 01:23:53 +06:00
Mend Renovate
bcff2262b3
Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1 (#1596) 2024-12-22 01:23:41 +06:00
Mend Renovate
04454ecdbe
Update dependency io.mockk:mockk to v1.13.14 (#1601) 2024-12-22 01:21:41 +06:00
Mend Renovate
69320e4d09
Migrate renovate config (#1572)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-12-13 02:39:46 +06:00
Mend Renovate
e86aeee9c4
Update moko-resources to v0.24.4 (#1553) 2024-12-08 15:56:05 +00:00
Mend Renovate
be37f214d8
Update dependency com.google.firebase:firebase-bom to v33.7.0 (#1545) 2024-12-07 16:22:27 +00:00
Mend Renovate
1a833e88b1
Update dependency com.android.tools.build:gradle to v8.7.3 (#1535) 2024-12-07 22:10:44 +06:00
Mend Renovate
4c84878adc
Update dependency com.pinterest.ktlint:ktlint-cli to v1.5.0 (#1540) 2024-12-07 22:10:30 +06:00
Mend Renovate
054198e78f
Update dependency org.jsoup:jsoup to v1.18.3 (#1533) 2024-12-07 22:10:04 +06:00
Mend Renovate
d522d81164
Update kotlin monorepo to v2.1.0 (#1518) 2024-12-07 22:09:47 +06:00
AntsyLich
40fe5f8437
Update CHANGELOG.md 2024-12-06 23:32:51 +06:00
AntsyLich
3a100c7816
Release v0.17.1 2024-12-06 23:12:20 +06:00
AntsyLich
ead9c0cd47
Replace project icon 2024-12-06 23:03:00 +06:00
Weblate (bot)
b4ad9ae063
Translations update from Hosted Weblate (#1531)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Illia Stoianov <Walrus_Morj@protonmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
2024-12-06 22:55:26 +06:00
MajorTanya
7f2cfb5eb2
Always use software bitmap on certain devices (#1543)
* Include Coil's broken hardware bitmap device list

Declares all listed devices as unable to use hardware bitmaps.

Might fix #1541.

* Hide Hardware Bitmap Threshold setting if unusable

This hides the setting from the UI if the user's device in on Coil's
list of devices with problematic hardware bitmap implementations.

Also moved HARDWARE_BITMAP_UNSUPPORTED into the ImageUtil as a
property for more ergonomic access across the project.

* Add missing negation

* Update CHANGELOG.md

* Update CHANGELOG.md

* Needs to be and not or

Also fix typo in CHANGELOG.md

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-12-06 21:14:18 +06:00
Mend Renovate
cc96f859bc
Update GitHub Actions (#1494) 2024-12-01 02:59:26 +06:00
AntsyLich
386a714ffe
Update CHANGELOG.md (#1434) 2024-12-01 02:59:06 +06:00
Weblate (bot)
a807722838
Translations update from Hosted Weblate (#1423)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bg/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/da/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Akhil Raj <89210430+akhi07rx@users.noreply.github.com>
Co-authored-by: AntsyLich <antsylich@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Horace Johnson <horacejohnson99@gmail.com>
Co-authored-by: Igor Coimbra Carvalheira <igorccarvalheira111@gmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Leandro Cândido <123888466+marshfellow42@users.noreply.github.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: TheKingTermux <achmadmaulana0233@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2024-12-01 02:39:14 +06:00
MajorTanya
3bd8d3ecb7
Add a Honor system app to list of invalid browsers (#1520)
Closes #1348.

Specifically adds com.hihonor.android.internal.app to the list of
invalid browsers. It's very similar to the existing entry for Huawei,
so it stands to reason it is the same/similar problem as with Huawei's
internal app.
2024-11-28 02:13:57 +06:00
Mend Renovate
8ea95cb27f
Update dependency org.jsoup:jsoup to v1.18.2 (#1515) 2024-11-27 14:27:09 +06:00
Mend Renovate
e280fd63b6
Update dependency io.coil-kt.coil3:coil-bom to v3.0.4 (#1510) 2024-11-26 13:58:26 +06:00
Mend Renovate
addb4ae9ad
Update dependency gradle to v8.11.1 (#1475) 2024-11-21 07:43:12 +06:00
AntsyLich
d6dfd24548
Improve hardware bitmap threshold option
Also `spotlessApply`
2024-11-21 07:14:18 +06:00
Cuong-Tran
88aff2c77f
Fix app update error notification disappearing (#1476) 2024-11-20 20:07:34 +06:00
AntsyLich
81effea01c
Slightly tweak Preference.PreferenceItem.CustomPreference 2024-11-20 17:54:30 +06:00
AntsyLich
9aef08c333
Fix loading screen not appearing when changing query in browser screen
Fixes #1438
Closes #1441
2024-11-20 17:54:30 +06:00
AntsyLich
dcddac5daa
Add option to lower the threshold for hardware bitmaps
Closes #1436
Closes #1486
2024-11-20 17:54:29 +06:00
AntsyLich
e6d96bd348
Switch to hardware bitmap in reader only if device can handle it
Closes #1460
2024-11-20 17:18:32 +06:00
AntsyLich
1909126921
Revert "Add option to always use SSIV for image decoding"
This reverts commit bb4d9fc81a.
2024-11-17 02:51:48 +06:00
Cuong-Tran
36d5ee0763
Fix reader transition color scheme in auto background mode (#1487) 2024-11-17 02:30:48 +06:00
Mend Renovate
5a91d5c611
Update paging.version to v3.3.4 (#1481) 2024-11-16 02:13:56 +06:00
Mend Renovate
e332590b1b
Update dependency androidx.viewpager:viewpager to v1.1.0-rc01 (#1480) 2024-11-16 02:13:06 +06:00
Mend Renovate
0106750503
Update GitHub Actions (#1477) 2024-11-15 18:47:37 +06:00
Mend Renovate
39982c4063
Update dependency io.coil-kt.coil3:coil-bom to v3.0.3 (#1485) 2024-11-15 18:46:45 +06:00
Mend Renovate
d1a970e3f3
Update dependency io.coil-kt.coil3:coil-bom to v3.0.2 (#1469) 2024-11-12 02:24:15 +06:00
Cuong-Tran
9df21583dc
Fix crash after removing last category while it's active in library (#1450) 2024-11-07 20:20:27 +06:00
AntsyLich
57e6e198b8
Update dependency androidx.work:work-runtime to v2.10.0 2024-11-07 20:07:46 +06:00
Mend Renovate
3a648e4fa5
Update dependency com.android.tools:desugar_jdk_libs to v2.1.3 (#1453) 2024-11-07 17:35:17 +06:00
Mend Renovate
6159bc3636
Update dependency io.coil-kt.coil3:coil-bom to v3.0.1 (#1454) 2024-11-07 17:33:07 +06:00
Mend Renovate
3cfc2be104
Update dependency com.pinterest.ktlint:ktlint-cli to v1.4.1 (#1449) 2024-11-06 00:48:00 +06:00
Mend Renovate
9580a00aa6
Update dependency androidx.compose:compose-bom to v2024.10.01 (#1424) 2024-11-05 22:04:10 +06:00
Mend Renovate
cb2b0464d0
Update dependency androidx.core:core-ktx to v1.15.0 (#1417) 2024-11-05 20:51:16 +06:00
Mend Renovate
ef7992f912
Update dependency com.android.tools.build:gradle to v8.7.2 (#1428) 2024-11-05 20:48:47 +06:00
Mend Renovate
261bbef997
Update softprops/action-gh-release action to v2.0.9 (#1425) 2024-11-05 20:48:15 +06:00
Mend Renovate
a5349a881b
Update dependency io.coil-kt.coil3:coil-bom to v3.0.0 (#1444) 2024-11-05 20:47:47 +06:00
Mend Renovate
2ca2cec02b
Update xml.serialization.version to v0.90.3 (#1446) 2024-11-05 20:46:14 +06:00
AntsyLich
2f4bb7cadb
Cleanup some code 2024-11-02 21:28:43 +06:00
AntsyLich
bb4d9fc81a
Add option to always use SSIV for image decoding 2024-11-02 21:24:27 +06:00
AntsyLich
ee134fce58
Update organization name in readme license snippet 2024-11-02 20:00:34 +06:00
Weblate (bot)
79e711efc2
Translations update from Hosted Weblate (#1111)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Akhil Raj <89210430+akhi07rx@users.noreply.github.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: AntsyLich <antsylich@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Chiro-kun <chirokun863@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eji-san <ejierubani@gmail.com>
Co-authored-by: Eren Eroğlu <ereneroglum@yahoo.com>
Co-authored-by: Fadhil Muhammad <alpanumerik1@gmail.com>
Co-authored-by: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: HDYOU <308485965@qq.com>
Co-authored-by: Homura Akemi <amber_c001@protonmail.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Kryptox <info.kryptox@gmail.com>
Co-authored-by: Leandro Cândido <123888466+marshfellow42@users.noreply.github.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Marco Espinoza <maviesco@gmail.com>
Co-authored-by: Milihraim <kirill06678@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: N. Hao <nguyenviethao2002@gmail.com>
Co-authored-by: NGB-Was-Taken <myalternate34@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Noah Kenzie Rodriguez-Beus <noahbeus@protonmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: SBS1313 <simonsaade005@gmail.com>
Co-authored-by: Saft Octavian <saftoctavian@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Valerio Marini <marinivalerio97@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: altinat <al@altqx.com>
Co-authored-by: altinat <altinat@duck.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gekka <1778962971@qq.com>
Co-authored-by: orkan gökçe alaz aşina <examplehuman@outlook.com>
Co-authored-by: phlostically <phlostically@mailinator.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: 赤星悠太 <yuta1219aka@gmail.com>
2024-10-31 20:19:19 +06:00
AntsyLich
a1c6089791
Address some build warnings and cleanup (#1412) 2024-10-31 20:17:27 +06:00
AntsyLich
06efc3b25c
Fix long strip images not loading in some old devices
Fixes #1398
2024-10-31 19:19:32 +06:00
AntsyLich
f508d10ad1
Fix a rare crash when invoking "Mark previous as read" action
Closes #1421
2024-10-31 19:19:24 +06:00
AntsyLich
22d8aad598
Auto format extension repo URLs
Closes #1392
Closes #1393
2024-10-31 19:17:12 +06:00
AntsyLich
76dcf90340
Bump default user agent 2024-10-31 18:28:26 +06:00
Mend Renovate
f33a6d2520
Update dependency io.coil-kt.coil3:coil-bom to v3.0.0-rc02 (#1401) 2024-10-31 01:53:51 +06:00
Mend Renovate
41ae8505fe
Update actions/dependency-review-action action to v4.4.0 (#1402) 2024-10-31 01:53:12 +06:00
Mend Renovate
2914d166fe
Update dependency androidx.constraintlayout:constraintlayout to v2.2.0 (#1416) 2024-10-30 19:52:51 +00:00
Mend Renovate
328ec8c752
Update lifecycle.version to v2.8.7 (#1415) 2024-10-30 19:52:37 +00:00
MajorTanya
78f9a84b14
Some improvements to Bangumi tracker search (#1396)
In short:
- fetch & show actual summary
- fallback to "name" if "name_cn" is empty
- request larger responseGroup to get & display the summary & rating
- add type filter query param to make Bangumi filter, not us

Previously, we only displayed the "name" in the summary area and used
"name_cn" as the entry name. However, "name_cn" (Chinese name) can be
an empty string at times, resulting in an awkward looking search
result list where some, many, or even all the results have no title
displayed and only show the "name" (Japanese name) in the summary
area. This has been solved by using "name" as a fallback value should
"name_cn" be empty.

If a Chinese name is available, the original name is prepended to the
summary with the addition "作品原名:" (meaning "original series title").

By using the "responseGroup=large" query parameter, we can request
the required data we need to display the actual summary for an entry
and the entry's average rating.
The "name" is prepended to the summary contents, if any exist, so it
is still accessible for series identification if a "name_cn" exists
too and was used for the result title.

Adding the "type=1" filter query parameter means Bangumi will only
return entries of type 1 ("book") instead of all types and Mihon
needing to filter, resulting in potentially missed entry matches.
2024-10-31 01:52:18 +06:00
Mend Renovate
eedece5adf
Update dependency androidx.annotation:annotation to v1.9.1 (#1413) 2024-10-31 01:46:31 +06:00
Mend Renovate
9d6ddb5d91
Update dependency androidx.viewpager:viewpager to v1.1.0-beta01 (#1414) 2024-10-31 01:44:42 +06:00
AntsyLich
b8b053b1d7
Switch to spotless 7.0.0 Beta 4 2024-10-30 19:50:17 +06:00
MajorTanya
ed9e13a365
Fix sporadically recurring spotless CI failure (#1407)
Somehow this specific issue keeps getting flagged by unrelated PRs'
CI runs (but only sometimes? Somehow? Other times the CI run would
succeed with no spotless issues.)

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-30 13:48:52 +00:00
AntsyLich
371c1432e2
Here lies "currentTab was used multiple times"
Fixes #282
2024-10-29 21:46:07 +06:00
AntsyLich
38d5fc9160
Release v0.17.0 2024-10-26 23:37:41 +06:00
AntsyLich
9454fe4482
Update CHANGELOG.md (#1349)
Co-authored-by: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com>
2024-10-26 21:49:44 +06:00
Cuong-Tran
6de06419f8
Fix app crash when removing tracked entry from tracker (#1380) 2024-10-26 20:12:34 +06:00
Roshan Varughese
fc2f339ea1
Allow completely disabling "Update tracker" snackbar on mark as read (#1374)
Also fixes #1369
2024-10-26 19:16:39 +06:00
Cuong-Tran
264030d6ec
Add libs.material to presentation-widget (#1373)
Fixes some build issues
2024-10-26 09:02:07 +06:00
AntsyLich
140083ee39
Update dependency com.pinterest.ktlint:ktlint-cli to v1.4.0
Co-authored-by: Mend Renovate <bot@renovateapp.com>
2024-10-26 07:40:57 +06:00
Mend Renovate
2bf7ef5d18
Update actions/setup-java action to v4.5.0 (#1366) 2024-10-26 07:23:13 +06:00
AntsyLich
df9fff60da
Cleanup Slider usage 2024-10-26 07:15:01 +06:00
Mend Renovate
aae0e3459c
Update dependency me.zhanghai.android.libarchive:library to v1.1.4 (#1378) 2024-10-26 03:55:19 +06:00
Cuong-Tran
f7752a98b2
Avoid blocking call to load categories in settings (#1364) 2024-10-24 23:51:47 +06:00
abdurisaq
2ba7ed3280
Fix settings SliderItem steps count (#1356) 2024-10-24 12:59:22 +00:00
Roshan Varughese
c153ac01f5
Rework Auto Track on Mark as Read (#1365) 2024-10-24 12:23:28 +00:00
Mend Renovate
47b0e9d7be
Pin actions/upload-artifact action to b4b15b8 (#1363)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-24 03:56:06 +06:00
AntsyLich
d4bf19f957
Make renovate group github action deps 2024-10-24 03:35:46 +06:00
Mend Renovate
01b44c0458
Update actions/checkout action to v4.2.2 (#1361) 2024-10-24 01:52:12 +06:00
Mend Renovate
e1e3ca7a56
Update actions/dependency-review-action action to v4.3.5 (#1354) 2024-10-24 01:51:55 +06:00
Mend Renovate
78d2cc75d5
Update dependency com.google.firebase:firebase-bom to v33.5.1 (#1362) 2024-10-24 01:51:39 +06:00
AntsyLich
c550a81598
Update shizuku.version to v13.1.0 2024-10-22 03:18:35 +06:00
Mend Renovate
0be36a10c3
Update dependency com.google.firebase:firebase-bom to v33.5.0 (#1352) 2024-10-21 19:53:17 +00:00
Mend Renovate
e16c3953c7
Update dependency org.junit.jupiter:junit-jupiter to v5.11.3 (#1351) 2024-10-22 01:49:40 +06:00
AntsyLich
f3a2f566c8
Pass uncaught exception to default handler in GlobalExceptionHandler
Fixes #1347
2024-10-19 22:51:01 +06:00
AntsyLich
15e3f28aa3
Rework Firebase setup
Fixes #1332
Closes #1339
2024-10-19 21:22:04 +06:00
AntsyLich
3bf70b230f
Address deprecation, suggestion and spotless 2024-10-19 20:19:06 +06:00
AntsyLich
eb3bea8150
Revert "Tweak Preference.collectAsState"
This reverts commit 3bddb55385.

Fixes #1341
2024-10-19 20:02:15 +06:00
Mend Renovate
5612ae0149
Update dependency androidx.compose:compose-bom to v2024.10.00 (#1338) 2024-10-19 20:00:56 +06:00
Mend Renovate
dbf6ad2ca7
Update xml.serialization.version to v0.90.2 (#1331)
* Update xml.serialization.version to v0.90.2

* Fix build

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-19 20:00:30 +06:00
AntsyLich
d2afbfe4ed
Change "Invalidate downloads index" to "Reindex downloads" 2024-10-19 17:06:29 +06:00
Mend Renovate
337806d9e1
Update dependency androidx.annotation:annotation to v1.9.0 (#1336) 2024-10-19 16:19:39 +06:00
Mend Renovate
443f6e0ae5
Update dependency androidx.glance:glance-appwidget to v1.1.1 (#1335) 2024-10-19 16:19:07 +06:00
Mend Renovate
572ee2f02a
Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.3 (#1334) 2024-10-19 16:18:38 +06:00
Mend Renovate
ba1343bed8
Update dependency androidx.activity:activity-compose to v1.9.3 (#1333) 2024-10-19 16:17:55 +06:00
FlaminSarge
9f3d5d13d4
[skip ci] Update i18n readme (#1328) 2024-10-15 19:14:52 +06:00
Mend Renovate
48166b9b52
Update dependency com.android.tools.build:gradle to v8.7.1 (#1326) 2024-10-15 05:02:45 +06:00
AntsyLich
2e2c8d36c1
Make sure random library sort is at the bottom 2024-10-15 05:00:56 +06:00
AntsyLich
788235feec
Reorder reader menu overflow items 2024-10-15 03:57:58 +06:00
AntsyLich
afa5002988
Cleanup .gitignore files 2024-10-15 03:39:48 +06:00
AntsyLich
9503082d44
Fix PR build check 2024-10-15 02:13:37 +06:00
Roshan Varughese
de36357da8
Add option to backup non-library read entries (#1324)
Co-authored-by: jobobby04 <jobobby04@gmail.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-14 16:30:23 +00:00
AntsyLich
eb6092bd0c
Adjust expandable fab animation
Co-authored-by: p
2024-10-13 23:06:02 +06:00
AntsyLich
32d2c2ac1b
Refrain from running spotless on weblate files
Those are akin to generated files and are likely to not follow our formatting
2024-10-13 23:02:35 +06:00
AntsyLich
4051f180a2
Run PR check when base strings are changed 2024-10-13 20:50:35 +06:00
brewkunz
3ed8a91c7b
Fix EnhancedTracker not auto binding when adding manga to library (#1298) 2024-10-13 20:32:29 +06:00
Roshan Varughese
87db3f90de
Confirmation dialog when removing privately installed extensions (#1320)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-13 18:48:00 +06:00
Mend Renovate
0a4ad89b99
Update dependency me.zhanghai.android.libarchive:library to v1.1.3 (#1321) 2024-10-13 18:47:31 +06:00
Jack Hamilton
a72db41bf1
Added random library sort (#1317)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-13 13:51:34 +06:00
Roshan Varughese
6b2bba4e54
Add Quantity Badge to Upcoming Screen (#1250)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-10-12 17:51:34 +06:00
Roshan Varughese
7c7af72f8c
Add option to opt out of Analytics and Crashlytics (#1237) 2024-10-12 06:46:28 +06:00
AntsyLich
c8bb78d91a
Tweak profile compilation status output
Co-authored-by: p
2024-10-12 06:23:37 +06:00
AntsyLich
2ba3f0612c
Remove usage of deprecated accompanist SystemUiController
Co-authored-by: p
2024-10-12 06:22:34 +06:00
AntsyLich
f84d9a08b4
ChapterNavigator: dispatch page change only when needed
Co-authored-by: p
2024-10-12 05:12:38 +06:00
AntsyLich
37419cdc26
Bump compile sdk to 35
Co-authored-by: p
2024-10-12 05:11:58 +06:00
AntsyLich
481cfedf08
Update resources exclusion rules
Co-authored-by: p
2024-10-12 05:11:16 +06:00
AntsyLich
9b8ab6acc2
Adjust distinct checker in WidgetManager and run on default dispatcher
Co-authored-by: p
2024-10-12 05:09:51 +06:00
AntsyLich
3bddb55385
Tweak Preference.collectAsState
Co-authored-by: p
2024-10-12 05:02:56 +06:00
AntsyLich
2beb89d531
Cleanup LibraryScreenModel LibraryMap.applySort and some more 2024-10-12 05:00:56 +06:00
Mend Renovate
016f627fb0
Update kotlin monorepo to v2.0.21 (#1314) 2024-10-10 18:21:31 +06:00
brewkunz
44aab7a243
Retain remote last chapter read if it's higher than the local one for EnhancedTracker (#1301) 2024-10-10 18:15:06 +06:00
Mend Renovate
a2dc88965b
Update dependency io.mockk:mockk to v1.13.13 (#1313) 2024-10-09 21:42:16 +06:00
AntsyLich
aa998071a1
Update renovate configuration
- Remove package rule for "dev.chrisbanes.compose:compose-bom"
- Disable semantic commits
2024-10-09 03:31:25 +06:00
Mend Renovate
8113b77f1e
fix(deps): update dependency io.coil-kt.coil3:coil-bom to v3.0.0-rc01 (#1308) 2024-10-08 19:57:20 +06:00
Mend Renovate
6adfa4fd0f
chore(deps): update actions/checkout action to v4.2.1 (#1304) 2024-10-08 19:46:40 +06:00
Secozzi
76e0aba70c
Fix AniList ALSearchItem.status nullibility (#1297) 2024-10-06 04:09:49 +06:00
Mend Renovate
f7fbc93833
fix(deps): update dependency androidx.compose:compose-bom to v2024.09.03 (#1288) 2024-10-05 06:16:01 +06:00
Mend Renovate
85ee9c6686
fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.2 (#1294) 2024-10-05 06:13:10 +06:00
Mend Renovate
c72c07f355
fix(deps): update dependency androidx.profileinstaller:profileinstaller to v1.4.1 (#1289) 2024-10-05 06:11:51 +06:00
Mend Renovate
6984e0465b
fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.3.2 (#1287) 2024-10-05 06:11:30 +06:00
Mend Renovate
3ca989eae8
fix(deps): update dependency com.google.firebase:firebase-bom to v33.4.0 (#1285) 2024-10-05 06:10:03 +06:00
Mend Renovate
cca33481dd
fix(deps): update dependency com.android.tools.build:gradle to v8.7.0 (#1284) 2024-10-02 03:26:28 +06:00
renovate[bot]
f7c8f1801e
chore(deps): update dependency gradle to v8.10.2 (#1254)
* chore(deps): update dependency gradle to v8.10.2

* Update binaries

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-09-28 06:36:10 +06:00
renovate[bot]
112b68b782
fix(deps): update dependency androidx.compose:compose-bom to v2024.09.02 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 06:16:34 +06:00
renovate[bot]
2dd02b73d6
fix(deps): update dependency org.junit.jupiter:junit-jupiter to v5.11.1 (#1262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 02:42:04 +06:00
renovate[bot]
369df527b2
chore(deps): update actions/checkout action to v4.2.0 (#1266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 02:41:54 +06:00
renovate[bot]
d04eeface9
fix(deps): update dependency me.zhanghai.android.libarchive:library to v1.1.2 (#1255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 01:22:43 +06:00
renovate[bot]
dde942df4e
chore(deps): update actions/setup-java action to v4.4.0 (#1259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-25 01:22:21 +06:00
215 changed files with 4000 additions and 1245 deletions

View File

@ -53,7 +53,7 @@ body:
label: Mihon version label: Mihon version
description: You can find your Mihon version in **More → About**. description: You can find your Mihon version in **More → About**.
placeholder: | placeholder: |
Example: "0.16.5" Example: "0.17.1"
validations: validations:
required: true required: true
@ -96,7 +96,7 @@ body:
required: true required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. - label: I have updated the app to version **[0.17.1](https://github.com/mihonapp/mihon/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -31,7 +31,7 @@ body:
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. - label: I have updated the app to version **[0.17.1](https://github.com/mihonapp/mihon/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@ -1,14 +1,13 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"], "extends": ["config:recommended"],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"semanticCommits": "disabled",
"packageRules": [ "packageRules": [
{ {
"groupName": "Compose BOM (Alpha)", "groupName": "GitHub Actions",
"matchPackageNames": [ "matchManagers": ["github-actions"],
"dev.chrisbanes.compose:compose-bom" "pinDigests": true,
], },
"ignoreUnstable": false ],
}
]
} }

View File

@ -1,10 +1,13 @@
name: PR build check name: PR build check
on: on:
pull_request: pull_request:
paths-ignore: paths:
- '**.md' - '**'
- 'i18n/src/commonMain/moko-resources/**/strings.xml' - '!**.md'
- 'i18n/src/commonMain/moko-resources/**/plurals.xml' - '!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'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@ -20,34 +23,34 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with: with:
java-version: 17 java-version: 17
distribution: adopt distribution: adopt
- name: Set up gradle - name: Set up gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
- name: Build app and run unit tests - name: Build app and run unit tests
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: arm64-v8a-${{ github.sha }} name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
- name: Upload mapping - name: Upload mapping
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: mapping-${{ github.sha }} name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/standardRelease path: app/build/outputs/mapping/standardRelease

View File

@ -17,35 +17,35 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 uses: gradle/actions/wrapper-validation@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
- name: Setup Android SDK - name: Setup Android SDK
run: | run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3" ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0 uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with: with:
java-version: 17 java-version: 17
distribution: adopt distribution: adopt
- name: Set up gradle - name: Set up gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
- name: Build app and run unit tests - name: Build app and run unit tests
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: arm64-v8a-${{ github.sha }} name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
- name: Upload mapping - name: Upload mapping
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: mapping-${{ github.sha }} name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/standardRelease path: app/build/outputs/mapping/standardRelease
@ -95,7 +95,7 @@ jobs:
- name: Create Release - name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
with: with:
tag_name: ${{ env.VERSION_TAG }} tag_name: ${{ env.VERSION_TAG }}
name: Mihon ${{ env.VERSION_TAG }} name: Mihon ${{ env.VERSION_TAG }}
@ -122,4 +122,4 @@ jobs:
draft: true draft: true
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.PAT }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

24
.gitignore vendored
View File

@ -1,18 +1,16 @@
# Build files
.gradle .gradle
.kotlin .kotlin
/local.properties build
/.idea/workspace.xml
.DS_Store # IDE files
.idea/*
!.idea/icon.png
*iml
*.iml *.iml
.idea/*
!.idea/icon.svg
/captures
# Built files # Configuration files
*/build local.properties
/build
*.apk
app/**/output.json
# Unnecessary file # macOS specific files
*.swp .DS_Store

BIN
.idea/icon.png generated

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

6
.idea/icon.svg generated Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 432 432">
<circle cx="216" cy="216" r="216" fill="#2e3943"/>
<path fill="#f2faff" d="M398.073 216c0 97.433-81.517 176.419-182.073 176.419-100.556 0-182.073-78.986-182.073-176.419S115.444 39.581 216 39.581c100.556 0 182.073 78.986 182.073 176.419Z"/>
<path fill="#7ebbed" fill-rule="evenodd" d="M216 359.34c81.702 0 147.934-64.175 147.934-143.34S297.702 72.66 216 72.66 68.065 136.835 68.065 216 134.298 359.34 216 359.34zm0 33.079c100.556 0 182.073-78.986 182.073-176.419S316.556 39.581 216 39.581C115.444 39.581 33.927 118.567 33.927 216S115.444 392.419 216 392.419z" clip-rule="evenodd"/>
<path fill="#031019" d="m155.273 168.033-1.227-28.215c3.68.7 8.063.875 18.052.875 12.092 0 28.041-.7 36.279-1.752 3.504-.35 4.907-.876 7.185-2.103l18.928 16.124c-1.753 2.453-2.279 3.505-4.207 8.412-1.576 3.856-8.762 26.113-11.567 35.577 12.97 2.63 20.155 4.557 29.97 8.588 1.226-8.588 1.401-13.144 1.401-28.742 0-4.03-.175-6.31-.7-9.99l30.495 1.051c-.877 4.207-1.052 5.959-1.227 12.794-.701 16.475-1.403 24.361-3.154 36.279 12.092 6.134 12.092 6.134 18.226 9.464 3.154 1.752 3.855 2.102 5.959 2.804l-10.165 32.773c-4.908-4.381-11.743-9.113-21.732-14.721-8.763 20.855-23.31 36.103-45.392 48.195-7.36-9.814-12.97-15.772-21.907-22.783 12.969-6.134 18.928-9.99 25.763-16.475 6.66-6.484 11.04-12.793 15.247-22.258-11.217-5.082-18.403-7.36-30.846-9.989-7.185 21.382-12.969 35.052-18.051 43.29-6.835 11.04-16.124 16.824-26.815 16.824-8.237 0-16.65-3.68-22.784-9.99-7.01-7.185-10.69-17.175-10.69-28.742 0-17.176 8.238-32.072 22.609-41.361 9.288-5.959 19.103-8.588 34.7-9.465 3.155-10.34 5.785-19.278 8.238-29.267-7.712.701-17.35 1.227-29.093 1.752-6.309.175-8.412.35-13.495 1.051zm26.64 53.279c-8.238 1.402-13.145 4.031-17.527 9.64-3.33 3.855-4.907 8.412-4.907 13.32 0 5.432 2.63 9.464 5.959 9.464 4.03 0 8.588-9.114 16.475-32.424z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -2,133 +2,291 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is a modified version of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `Added` - for new features.
- `Changed ` - for changes in existing functionality.
- `Improved` - for enhancement or optimization in existing functionality.
- `Removed` - for now removed features.
- `Fixed` - for any bug fixes.
- `Other` - for technical stuff.
## [Unreleased] ## [Unreleased]
### Added ### Added
- Add option to always decode long strip images with SSIV
## [v0.17.1] - 2024-12-06
### Changed
- Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`76dcf90`](https://github.com/mihonapp/mihon/commit/76dcf903403d565056f44c66d965c1ea8affffc3))
### Improved
- Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396))
- Extension repo URLs are now auto-formatted ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([`22d8aad`](https://github.com/mihonapp/mihon/commit/22d8aad598bea8f00f2831779e45a6645392ca0f))
### Fixed
- Fix "currentTab was used multiple times" ([@AntsyLich](https://github.com/AntsyLich)) ([`371c143`](https://github.com/mihonapp/mihon/commit/371c1432e218f6dcf129f05405dceb2cd351c647))
- Fix a rare crash when invoking "Mark previous as read" action ([@AntsyLich](https://github.com/AntsyLich)) ([`f508d10`](https://github.com/mihonapp/mihon/commit/f508d10ad13560d7316df8642bc93fe66c05b9a8))
- Fix long strip images not loading in some old devices ([@AntsyLich](https://github.com/AntsyLich)) ([`06efc3b`](https://github.com/mihonapp/mihon/commit/06efc3b25c5af51f42448af27a269ee459d9093d))
- Switch to hardware bitmap in reader only if device can handle it ([@AntsyLich](https://github.com/AntsyLich)) ([`e6d96bd`](https://github.com/mihonapp/mihon/commit/e6d96bd348ea5d18a005d6465222ad5f5123103e))
- Add option to lower the threshold for hardware bitmaps ([@AntsyLich](https://github.com/AntsyLich)) ([`dcddac5`](https://github.com/mihonapp/mihon/commit/dcddac5daaff3ec89c8507c35dc13d345ffdb6d7))
- Improve hardware bitmap threshold option ([@AntsyLich](https://github.com/AntsyLich)) ([`d6dfd24`](https://github.com/mihonapp/mihon/commit/d6dfd24548eaa05a8c3e478068fe2e08f2ee4473))
- Always use software bitmap on certain devices ([@MajorTanya](https://github.com/MajorTanya)) ([#1543](https://github.com/mihonapp/mihon/pull/1543))
- Fix crash after removing last category while it's active in library ([@cuong-tran](https://github.com/cuong-tran)) ([#1450](https://github.com/mihonapp/mihon/pull/1450))
- Fix reader transition color scheme in auto background mode ([@cuong-tran](https://github.com/cuong-tran)) ([#1487](https://github.com/mihonapp/mihon/pull/1487))
- Fix app update error notification disappearing ([@cuong-tran](https://github.com/cuong-tran)) ([#1476](https://github.com/mihonapp/mihon/pull/1476))
- Fix browser not opening in some cases in Honor devices ([@AntsyLich](https://github.com/AntsyLich), [@MajorTanya](https://github.com/MajorTanya)) ([#1520](https://github.com/mihonapp/mihon/pull/1520))
## [v0.17.0] - 2024-10-26
### Added
- Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302)) - Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302))
- Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459)) - Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459))
- Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) - Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492))
- Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420)) - Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420))
- Only show upcoming updates in the future ([@sirlag](https://github.com/sirlag)) ([#606](https://github.com/mihonapp/mihon/pull/606))
- Add Quantity Badge to Upcoming Screen ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1250](https://github.com/mihonapp/mihon/pull/1250))
- Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742)) - Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742))
- Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar), [@null2264](https://github.com/null2264)) ([#949](https://github.com/mihonapp/mihon/pull/949), [#967](https://github.com/mihonapp/mihon/pull/967)) - Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar)) ([#949](https://github.com/mihonapp/mihon/pull/949))
- Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625)) - Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625))
- 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971)) - 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971))
- Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977)) - Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977))
- Save global search "Has result" choice ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649))
- Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003)) - Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003))
- Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101)) - Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101))
- A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3)) - A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3))
- Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110)) - Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110))
- Reorder reader menu overflow items ([@AntsyLich](https://github.com/AntsyLich)) ([`788235f`](https://github.com/mihonapp/mihon/commit/788235feeca241228eac0561339dd07b5ea0b77d))
- Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) - Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125))
- Add confirmation dialog when adding repo via URI ([@Animeboynz](https://github.com/Animeboynz)) ([#1158](https://github.com/mihonapp/mihon/pull/1158))
- Add "show entry" action to download notifications ([@mm12](https://github.com/mm12), [@AntsyLich](https://github.com/AntsyLich)) ([#1159](https://github.com/mihonapp/mihon/pull/1159))
- Option to update trackers when chapter marked as read ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1177](https://github.com/mihonapp/mihon/pull/1177), [#1365](https://github.com/mihonapp/mihon/pull/1365), [#1374](https://github.com/mihonapp/mihon/pull/1374))
- Toast to restart app when User-Agent is changed ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1204](https://github.com/mihonapp/mihon/pull/1204))
- Added more profile compilation status (p) ([`c8bb78d`](https://github.com/mihonapp/mihon/commit/c8bb78d91afc2824baaca999f0095559c49d1306))
- Add option to opt out of Analytics and Crashlytics ([@Animeboynz](https://github.com/Animeboynz)) ([#1237](https://github.com/mihonapp/mihon/pull/1237))
- Rework Firebase setup ([@AntsyLich](https://github.com/AntsyLich)) ([`15e3f28`](https://github.com/mihonapp/mihon/commit/15e3f28aa36bec3c31f212c572ab57ce960cc862))
- Added random library sort ([@jackhamilton](https://github.com/jackhamilton)) ([#1317](https://github.com/mihonapp/mihon/pull/1317))
- Make sure random library sort is at the bottom ([@AntsyLich](https://github.com/AntsyLich)) ([`2e2c8d3`](https://github.com/mihonapp/mihon/commit/2e2c8d36c1e23bf274c7c19f1242e14b0c7afbc1))
- Confirmation dialog when removing privately installed extensions ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1320](https://github.com/mihonapp/mihon/pull/1320))
- Option to backup non-library read entries ([@Animeboynz](https://github.com/Animeboynz), [@jobobby04](https://github.com/jobobby04), [@AntsyLich](https://github.com/AntsyLich)) ([#1324](https://github.com/mihonapp/mihon/pull/1324))
### Changed ### Changed
- Read archive files from memory instead of extracting files to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) - Read archive files from memory instead of temporarily extracting to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326))
- Try to get resource from Extension before checking in the app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433)) - Fix dual page split ([@FooIbar](https://github.com/FooIbar)) ([#485](https://github.com/mihonapp/mihon/pull/485))
- Default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) - Bump default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449))
- Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e)) - Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e))
- Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) - Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492))
- Extension trust system ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz) ([#570](https://github.com/mihonapp/mihon/pull/570), [#1057](https://github.com/mihonapp/mihon/pull/1057)) - Extension trust system
- Store extension repo details from `repo.json` in database ([@sirlag](https://github.com/sirlag)) ([#506](https://github.com/mihonapp/mihon/pull/506))
- Fix extension repo migration not triggering ([@AntsyLich](https://github.com/AntsyLich)) ([`9672ea8`](https://github.com/mihonapp/mihon/commit/9672ea8b1b06f464800e310c96e060ead182f7ca))
- Refactor the ExtensionRepoService to use DTOs ([@MajorTanya](https://github.com/MajorTanya)) ([#573](https://github.com/mihonapp/mihon/pull/573))
- Fix extension repo name is used to construct URL instead of baseUrl ([@MajorTanya](https://github.com/MajorTanya)) ([#572](https://github.com/mihonapp/mihon/pull/572))
- Fix crash with `TypeReference` issue when creating extension repo ([@AntsyLich](https://github.com/AntsyLich)) ([#574](https://github.com/mihonapp/mihon/pull/574), [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a))
- Fix mishap in [`e020ae5`](https://github.com/mihonapp/mihon/commit/e020ae5ed558e80742ef0ad8bfa0f69af0959d5a) ([@AntsyLich](https://github.com/AntsyLich)) ([`6965e59`](https://github.com/mihonapp/mihon/commit/6965e59a643c67a2bf81b3c69ec70268e5da5797))
- Backup and Restore ([@Animeboynz](https://github.com/Animeboynz)) ([#1057](https://github.com/mihonapp/mihon/pull/1057))
- Trust extension by repo ([@AntsyLich](https://github.com/AntsyLich)) ([#570](https://github.com/mihonapp/mihon/pull/570))-
- From M2 ripple to M3 ([@FooIbar](https://github.com/FooIbar)) ([#675](https://github.com/mihonapp/mihon/pull/675))
- Increased continue reading button size ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz)) ([`e17f70f`](https://github.com/mihonapp/mihon/commit/e17f70f7226ea031fc1f962c9dfea3e404ba53ad))
- Global search "Has result" choice is now sticky ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649))
- Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) - Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596))
- Rename backup restore error log file ([@AntsyLich](https://github.com/AntsyLich)) ([`2858ef8`](https://github.com/mihonapp/mihon/commit/2858ef835fec8d7278b1d0cad1b5664104d1e4b0))
- Keyboard type in add extension repo dialog ([@xbjfk](https://github.com/xbjfk)) ([#764](https://github.com/mihonapp/mihon/pull/764))
- Adjust collapse/open animation on manga description ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`1c16fc7`](https://github.com/mihonapp/mihon/commit/1c16fc79c2ac4c4be30308fed84ffb371dab5902))
- Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106)) - Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106))
- Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156)) - Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156))
- Hide keyboard when a Tracker SearchResultItem is clicked ([@Animeboynz](https://github.com/Animeboynz)) ([#1168](https://github.com/mihonapp/mihon/pull/1168))
- Enable 'Split Tall Images' by default ([@Smol-Ame](https://github.com/Smol-Ame)) ([#1185](https://github.com/mihonapp/mihon/pull/1185))
- Ignore "intent://" urls on webview ([@bapeey](https://github.com/bapeey)) ([#1193](https://github.com/mihonapp/mihon/pull/1193))
- Make reader chapter navigator slightly wider on small screens (p) ([#1202](https://github.com/mihonapp/mihon/pull/1202))
- Re-enable fetching chapters list for entries with licenced status ([@Animeboynz](https://github.com/Animeboynz)) ([#1230](https://github.com/mihonapp/mihon/pull/1230))
- Change casing for Extention Repos String ([@Animeboynz](https://github.com/Animeboynz)) ([#1248](https://github.com/mihonapp/mihon/pull/1248))
- Retain remote last chapter read if it's higher than the local one for EnhancedTracker ([@brewkunz](https://github.com/brewkunz)) ([#1301](https://github.com/mihonapp/mihon/pull/1301))
- Adjust expandable fab animation (p) ([`eb6092b`](https://github.com/mihonapp/mihon/commit/eb6092bd0cfa09694985a8bafdd8bbf2815190a1))
- "Invalidate downloads index" to "Reindex downloads" ([@AntsyLich](https://github.com/AntsyLich)) ([`d2afbfe`](https://github.com/mihonapp/mihon/commit/d2afbfe4ede283076aae40633c79c3f90b4390e7))
### Improvement ### Improved
- Long strip reader performance ([@FooIbar](https://github.com/FooIbar), [@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687)) - Reader performance
- Avoid unnecessary copying when processing reader image ([@FooIbar](https://github.com/FooIbar)) ([#691](https://github.com/mihonapp/mihon/pull/691))
- Significantly improve performance when loading extremely long images in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#692](https://github.com/mihonapp/mihon/pull/692))
- Use `Bitmap.Config.HARDWARE` if possible to improve image loading speed ([@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687))
- Improve preloading in long strip mode ([@FooIbar](https://github.com/FooIbar)) ([#1076](https://github.com/mihonapp/mihon/pull/1076))
- Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728)) - Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728))
- Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383)) - Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383))
- Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056)) - Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056))
### Removed
- Legacy download folder names no longer supported ([@AntsyLich](https://github.com/AntsyLich)) ([`e55e5f6`](https://github.com/mihonapp/mihon/commit/e55e5f6f64f872475d370d6ce0c186e2601776e4))
- Remove legacy broken source and history backup ([@AntsyLich](https://github.com/AntsyLich)) ([`518abf0`](https://github.com/mihonapp/mihon/commit/518abf032ccb9bb45d197927be2a5faca4167d29))
- Remove more unnecessary permissions from Firebase dependency ([@AntsyLich](https://github.com/AntsyLich)) ([`02af9b1`](https://github.com/mihonapp/mihon/commit/02af9b1acf9f590d29560bc3fc90d206e8e6e1af))
- Fix mishap in `02af9b1` ([@AntsyLich](https://github.com/AntsyLich)) ([`f22767d`](https://github.com/mihonapp/mihon/commit/f22767d863a0fa001f93f24092cd5ade87350502))
### Fixed ### Fixed
- Creating `ComicInfo.xml` file for local source ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325)) - Extracting `ComicInfo.xml` from local source archives ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325))
- Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1)) - Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1))
- Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494)) - Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494))
- Occasional black bar when scrolling in long strip reader ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563)) - Fix reader page image not being decoded until it's visible ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563))
- Reader chapter progress slider visuals ([@FooIbar](https://github.com/FooIbar)) ([#674](https://github.com/mihonapp/mihon/pull/674))
- Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) - `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42))
- Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767)) - Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767))
- Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783)) - Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783))
- Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976)) - Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976), [9a34ace](https://github.com/mihonapp/mihon/commit/9a34ace09c66274e6c2b3f9446058a0fa99d4bd0))
- Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726)) - Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726))
- Fix unexpected skips in strong skipping mode ([@FooIbar](https://github.com/FooIbar)) ([#940](https://github.com/mihonapp/mihon/pull/940))
- Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db)) - Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db))
- Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732)) - Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732))
- Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987)) - Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987))
- Fix login prompts despite being logged in to trackers in Manga screen ([@AntsyLich](https://github.com/AntsyLich)) ([`cbcd8bd`](https://github.com/mihonapp/mihon/commit/cbcd8bd6682023f728568f2b44da26124618aed7))
- JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993)) - JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993))
- Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030)) - Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
- Fix item disappearing when fast scrolling ([@cuong-tran](https://github.com/cuong-tran)) ([#1035](https://github.com/mihonapp/mihon/pull/1035))
- Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) - Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596))
- Crash on list with 0 item but only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083)) - Crash on list with only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083))
- Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084)) - Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084))
- MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098)) - MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098))
- Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0)) - Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0))
- Manga next update calculation when considering custom fetch interval ([@cuong-tran](https://github.com/cuong-tran)) ([#1206](https://github.com/mihonapp/mihon/pull/1206))
- WheelPicker Manual Input ([@Animeboynz](https://github.com/Animeboynz)) ([#1209](https://github.com/mihonapp/mihon/pull/1209))
- EnhancedTracker not auto binding when adding manga to library ([@brewkunz](https://github.com/brewkunz)) ([#1298](https://github.com/mihonapp/mihon/pull/1298))
- Step count in settings slider ([@abdurisaq](https://github.com/abdurisaq)) ([#1356](https://github.com/mihonapp/mihon/pull/1356))
- Freezing in some screens due to blocking call ([@cuong-tran](https://github.com/cuong-tran)) ([#1364](https://github.com/mihonapp/mihon/pull/1364))
- Crash when removing non-existent tracked entry from tracker ([@cuong-tran](https://github.com/cuong-tran)) ([#1380](https://github.com/mihonapp/mihon/pull/1380))
### Other
- Code cleanup
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
- Inside `WorkerInfoScreen` ([@AntsyLich](https://github.com/AntsyLich)) ([`5aec8f8`](https://github.com/mihonapp/mihon/commit/5aec8f8018236a38106483da08f9cbc28261ac9b))
- Inside `ChapterDownloadIndicator`, `MangaChapterListItem` ([@AntsyLich](https://github.com/AntsyLich)) ([`b7e091d`](https://github.com/mihonapp/mihon/commit/b7e091d5d039e00cababc7daf555280df6cf9c03))
- MangaCoverFetcher ([@ivaniskandar](https://github.com/ivaniskandar)) ([`1365695`](https://github.com/mihonapp/mihon/commit/13656959ae0606736f6ca9eb62699dc23e467c2f))
- Cleanup `LibraryScreenModel` `LibraryMap.applySort` and some more ([@AntsyLich](https://github.com/AntsyLich)) ([`2beb89d`](https://github.com/mihonapp/mihon/commit/2beb89d53163a6d288f8acdebe0f5d26fea8ab3e))
- Address `overridePendingTransition` deprecation ([@MajorTanya](https://github.com/MajorTanya)) ([#410](https://github.com/mihonapp/mihon/pull/410))
- Prioritize extension classes and files over app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433))
- Use compose pager implementation ([@ivaniskandar](https://github.com/ivaniskandar)) ([`84984ef`](https://github.com/mihonapp/mihon/commit/84984ef7e1d7242924120cd2f171cb9dd75bc916))
- Switch to coil3 from coil2 ([@ivaniskandar](https://github.com/ivaniskandar)) ([`f72b6e4`](https://github.com/mihonapp/mihon/commit/f72b6e4d7c1f2f93d705402e4d80c94160bef54d))
- Fix GIF not playing ([@jobobby04](https://github.com/jobobby04)) ([`59bedb3`](https://github.com/mihonapp/mihon/commit/59bedb33ff59ad5db1df2e93567a2266fb63eacc))
- Accommodate db for sync support ([@kaiserbh](https://github.com/kaiserbh)) ([#450](https://github.com/mihonapp/mihon/pull/450))
- Fix webtoon last visible item position calculation ([@FooIbar](https://github.com/FooIbar)) ([#562](https://github.com/mihonapp/mihon/pull/562))
- Migrate from `com.google.accompanist:accompanist-webview` to `io.github.kevinnzou:compose-webview` ([@sirlag](https://github.com/sirlag)) ([#569](https://github.com/mihonapp/mihon/pull/569))
- Rewrite migrations ([@ghostbear](https://github.com/ghostbear)) ([#577](https://github.com/mihonapp/mihon/pull/577))
- Further improve migration ([@ghostbear](https://github.com/ghostbear)) ([#588](https://github.com/mihonapp/mihon/pull/588))
- Fix migrations not running ([@ghostbear](https://github.com/ghostbear)) ([#604](https://github.com/mihonapp/mihon/pull/604))
- Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
- Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
- Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef))
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
- Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605))
- Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a))
- Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9))
- Upload PR build artifacts to GitHub ([@FooIbar](https://github.com/FooIbar)) ([#941](https://github.com/mihonapp/mihon/pull/941))
- Refactor archive support with libarchive ([@FooIbar](https://github.com/FooIbar)) ([#949](https://github.com/mihonapp/mihon/pull/949))
- Add safeguard to prevent ArchiveInputStream from being closed twice ([@null2264](https://github.com/null2264)) ([#967](https://github.com/mihonapp/mihon/pull/967))
- Move archive related code to :core:archive ([@AntsyLich](https://github.com/AntsyLich)) ([`bd7b354`](https://github.com/mihonapp/mihon/commit/bd7b35419861df6d426d6ec0a188391910d0f615))
- Replace detekt with ktlint via spotless ([@AntsyLich](https://github.com/AntsyLich)) ([#1130](https://github.com/mihonapp/mihon/pull/1130), [#1136](https://github.com/mihonapp/mihon/pull/1136), [#1138](https://github.com/mihonapp/mihon/pull/1138))
- Refrain from running spotless on weblate files ([@AntsyLich](https://github.com/AntsyLich)) ([`32d2c2a`](https://github.com/mihonapp/mihon/commit/32d2c2ac1bc224cbda2f09a4023d7d120ea0e954))
- Use feature flags in compose compiler plugin ([@AntsyLich](https://github.com/AntsyLich)) ([`8f9a325`](https://github.com/mihonapp/mihon/commit/8f9a325895bb7b94c2ec92dd969094fc30b3b5e2))- PagerPageHolder: lazy init loading indicator ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`a45eb5e`](https://github.com/mihonapp/mihon/commit/a45eb5e5288159dbbbbb5f92140ce0dd32a8f3ab))
- Collect MangaScreen state with lifecycle ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
- Add stable marker to Manga data class ([@AntsyLich](https://github.com/AntsyLich), [@ivaniskandar](https://github.com/ivaniskandar)) ([`03eb756`](https://github.com/mihonapp/mihon/commit/03eb756ecba0692d88d3a76254afc4c157fa225b))
- Use DTOs to parse tracking API responses ([@MajorTanya](https://github.com/MajorTanya)) ([#1103](https://github.com/mihonapp/mihon/pull/1103))
- Fix Kitsu ratingTwenty being typed as String ([@MajorTanya](https://github.com/MajorTanya)) ([#1191](https://github.com/mihonapp/mihon/pull/1191))
- Fix Kitsu `synopsis` nullability ([@MajorTanya](https://github.com/MajorTanya)) ([#1233](https://github.com/mihonapp/mihon/pull/1233))
- Fix AniList `ALSearchItem.status` nullibility ([@Secozzi](https://github.com/Secozzi)) ([#1297](https://github.com/mihonapp/mihon/pull/1297))
- Migrate some classpaths to gradle plugins ([@AntsyLich](https://github.com/AntsyLich)) ([`fc1c804`](https://github.com/mihonapp/mihon/commit/fc1c804bfda1d76c0399bbb6214e75b3def951cc))
- Add crashlytics to standard builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c611b9`](https://github.com/mihonapp/mihon/commit/3c611b95fb79e5ac972019b76c7b24f46a3087fd))
- Switch to stable compose ([@AntsyLich](https://github.com/AntsyLich)) ([`2baffa6`](https://github.com/mihonapp/mihon/commit/2baffa62cade1abd978d5fd03151b47fc87fd31e))
- Switch from inorichi injekt to kohesive Injekt ([@AntsyLich](https://github.com/AntsyLich)) ([#1205](https://github.com/mihonapp/mihon/pull/1205))
- Use custom injekt register with inorichi patch ([@AntsyLich](https://github.com/AntsyLich)) ([`83fd474`](https://github.com/mihonapp/mihon/commit/83fd4746eda1b99f35292b0c2211e606a421b3eb))
- Use TextFieldState in BasicTextField where applicable (p) ([#1201](https://github.com/mihonapp/mihon/pull/1201))
- Bump NDK version ([@AntsyLich](https://github.com/AntsyLich)) ([#1203](https://github.com/mihonapp/mihon/pull/1203))
- Move firebase permission removal to standard flavor ([@AntsyLich](https://github.com/AntsyLich)) ([`be671b4`](https://github.com/mihonapp/mihon/commit/be671b42cefd70180644e01bb065a18cb7701bf9))
- Adjust distinct checker in WidgetManager and run on default dispatcher (p) ([`9b8ab6a`](https://github.com/mihonapp/mihon/commit/9b8ab6acc25a5f99c9c5eebf9cc250975931c57c))
- Update resources exclusion rules (p) ([`481cfed`](https://github.com/mihonapp/mihon/commit/481cfedf08576cecfbb35616837bd8f627d8f959))
- Bump compile sdk to 35 (p) ([`37419cd`](https://github.com/mihonapp/mihon/commit/37419cdc26c2b5c4f8583fc2ba439b08fab42856))
- ChapterNavigator: dispatch page change only when needed (p) ([`f84d9a0`](https://github.com/mihonapp/mihon/commit/f84d9a08b4af768b1e9920c43cc445c86f5427fc))
- Remove usage of deprecated accompanist SystemUiController ([@AntsyLich](https://github.com/AntsyLich)) ([`2ba3f06`](https://github.com/mihonapp/mihon/commit/2ba3f0612c08c7021fed2f6d96cd538da2f34a13))
- Run PR check when base strings are changed ([@AntsyLich](https://github.com/AntsyLich)) ([`4051f18`](https://github.com/mihonapp/mihon/commit/4051f180a2e36e8a2cde6c55f0bea7952fdc4704))
- Fix PR build check ([@AntsyLich](https://github.com/AntsyLich)) ([`9503082`](https://github.com/mihonapp/mihon/commit/9503082d44b5bd868ee1bfc42741dc978d1d9047))
- Cleanup .gitignore files ([@AntsyLich](https://github.com/AntsyLich)) ([`afa5002`](https://github.com/mihonapp/mihon/commit/afa50029882655af8d5eea40aed7644fce4564d8))
- Pass uncaught exception to default handler in GlobalExceptionHandler (so it's reported to crashlytics) ([@AntsyLich](https://github.com/AntsyLich)) ([`f3a2f56`](https://github.com/mihonapp/mihon/commit/f3a2f566c8a09ab862758ae69b43da2a2cd8f1db))
## [v0.16.5] - 2024-04-09 ## [v0.16.5] - 2024-04-09
### Added ### Added
- Setting to install custom color profiles to get true colors ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) - Relative date for up to a week in the future ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415))
- Advance setting to install custom color profiles ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Changed ### Changed
- Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) - Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
### Fixed ### Fixed
- Fix wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402), [#415](https://github.com/mihonapp/mihon/pull/415)) - Wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402))
- Fix app infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411)) - Fix extra date header introduced by parent PR ([@sirlag](https://github.com/sirlag)) ([#415](https://github.com/mihonapp/mihon/pull/415))
- Fix crash on Pixel devices ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) - Fix build time in about screen displayed in UTC ([@AntsyLich](https://github.com/AntsyLich)) ([`aed53d3`](https://github.com/mihonapp/mihon/commit/aed53d3bdc85ce0e899fbb90b9f9cad0f1b86480))
- Fix crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) - App infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411))
- Fix crash in track date selection dialog ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5)) - Crash on Pixel devices (was introduced due to compose update) ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651))
- Fix dates for saved images on Samsung devices ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552)) - Crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466))
- Fix colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) - Crash when putting app in background while track date selection dialog is open ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5))
- Dates for saved images not following the specification (fixes date issue mainly on Samsung devices) ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552))
- Colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523))
## [v0.16.4] - 2024-02-26 ## [v0.16.4] - 2024-02-27
### Fixed ### Changed
- Circumvent MAL block ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) - Don't include custom user agent for MAL (circumvents MAL block) ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee))
## [v0.16.3] - 2024-01-30 ## [v0.16.3] - 2024-01-30
### Added ### Added
- Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271)) - Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271))
### Changed ### Changed
- Rename extension update error file to `mihon_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253)) - Hide display cutoff setting in reader settings sheet if fullscreen is disabled ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241))
- Hide display cutoff setting in reader settings sheet if fullscreen is off ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241)) - Library update error filename to `mihon_update_errors.txt` from `tachiyomi_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253))
### Fixed ### Fixed
- Fix bottom sheet display issues on non-Tablet UI ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) - Bottom sheet UI issues on non-tablet devices ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182))
- Fix crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272)) - Crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272))
- Fix newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275)) - Newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275))
- Fix error handling when refreshing MAL OAuth token ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f)) - Failing to refresh MAL token being inferred as token expiration ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f))
### Other
- Add `detekt` (kotlin code analyzer) to the project ([@theolm](https://github.com/theolm)) ([#216](https://github.com/mihonapp/mihon/pull/216))
## [v0.16.2] - 2024-01-28 ## [v0.16.2] - 2024-01-28
### Added
- Scanlator filter is now part of Backup ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166))
### Changed ### Changed
- Rename crash log filename to `mihon_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234)) - Backup now contains scanlator filter of a series ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166))
- App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a))
- Tracker OAuth client to Mihon's (fixes login issue for Shikimori tracker) ([@AntsyLich](https://github.com/AntsyLich)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5))
- Tracker user agents ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed)) ([`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5))
- Crash log filename to `mihon_crash_logs.txt` from `tachiyomi_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234))
- Don't try to refresh MAL token after refresh token expires ([@AntsyLich](https://github.com/AntsyLich)) ([`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba))
### Fixed ### Fixed
- "Flash screen on page change" Making the screen goes blank ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) - "Flash screen on page change" making the screen full black ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5))
- App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a)) - Faulty MangaUpdates score in database ([@AntsyLich](https://github.com/AntsyLich) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2)
- Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7)) - Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7))
- Inconsistent button height with some languages in "Data and storage" ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202)) - Inconsistent button height in "Data and storage" for some languages ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202))
- Fix chapter not being marked as read in some cases with Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219)) - Chapter not being marked as read locally when refreshing Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))
- And various tracker related fixes ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed), [@Secozzi](https://github.com/Secozzi)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2), [`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5), [`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba))
### Other
- Make `last_modified_at` field in database be `0` on insert ([@kaiserbh](https://github.com/kaiserbh)) ([#113](https://github.com/mihonapp/mihon/pull/113))
- Remove usage of `.not()` where possible in code ([@AntsyLich](https://github.com/AntsyLich)) ([`3940740`](https://github.com/mihonapp/mihon/commit/39407407f282dbb7fa972b12053c26b3e3bd66d8))
- Use type-safe project accessors ([@theolm](https://github.com/theolm)) ([#194](https://github.com/mihonapp/mihon/pull/194))
- Legacy tracker model properties now has the same type as the domain ones ([@AntsyLich](https://github.com/AntsyLich)) ([#245](https://github.com/mihonapp/mihon/pull/245))
## [v0.16.1] - 2024-01-18 ## [v0.16.1] - 2024-01-18
### Changed
- Branding to Mihon (for references we missed) ([@AntsyLich](https://github.com/AntsyLich)) ([`6539406`](https://github.com/mihonapp/mihon/commit/653940613d661eb371aab3b3c3a8181e4e308c43))
- Preview builds are now called Beta builds ([@AntsyLich](https://github.com/AntsyLich)) ([`3c3a1cd`](https://github.com/mihonapp/mihon/commit/3c3a1cd448ab1f653ddd12b2afe0cba38968d1b9))
### Fixed ### Fixed
- App Icon not filled ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130)) - App icon not following the [specification](https://developer.android.com/develop/ui/views/launch/icon_design_adaptive) ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130))
- MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa)) - MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa))
## [v0.16.0] - 2024-01-16 ## [v0.16.0] - 2024-01-16
### Changed
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
"The end of 立ち読み (Tachiyomi) is the beginning of みほん (Mihon)" [unreleased]: https://github.com/mihonapp/mihon/compare/v0.17.1...main
Credit to LinkCable, the icon designer, for this poetic quote. [v0.17.1]: https://github.com/mihonapp/mihon/compare/v0.17.0...v0.17.1
[v0.17.0]: https://github.com/mihonapp/mihon/compare/v0.16.5...v0.17.0
What's New?
Well, nothing, except you now you need Android 8+ to install the app.
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.16.5...HEAD
[v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5 [v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5
[v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4 [v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4
[v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3 [v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3
[v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2 [v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2
[v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1 [v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1
[v0.16.0]: https://github.com/mihonapp/mihon/releases/tag/v0.16.0 [v0.16.0]: https://github.com/mihonapp/mihon/compare/a9c7cbf...v0.16.0

View File

@ -68,7 +68,7 @@ The developer(s) of this application does not have any affiliation with the cont
<pre> <pre>
Copyright © 2015 Javier Tomás Copyright © 2015 Javier Tomás
Copyright © 2024 The Mihon Open Source Project Copyright © 2024 Mihon Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

3
app/.gitignore vendored
View File

@ -1,3 +0,0 @@
/build
*iml
*.iml

View File

@ -1,7 +1,8 @@
@file:Suppress("ChromeOsAbiSupport")
import mihon.buildlogic.getBuildTime import mihon.buildlogic.getBuildTime
import mihon.buildlogic.getCommitCount import mihon.buildlogic.getCommitCount
import mihon.buildlogic.getGitSha import mihon.buildlogic.getGitSha
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("mihon.android.application") id("mihon.android.application")
@ -28,8 +29,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "app.mihon" applicationId = "app.mihon"
versionCode = 7 versionCode = 9
versionName = "0.16.5" versionName = "0.17.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -108,13 +109,16 @@ android {
packaging { packaging {
resources.excludes.addAll( resources.excludes.addAll(
listOf( listOf(
"kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES", "META-INF/DEPENDENCIES",
"LICENSE.txt", "LICENSE.txt",
"META-INF/LICENSE", "META-INF/LICENSE",
"META-INF/LICENSE.txt", "META-INF/**/LICENSE.txt",
"META-INF/*.properties",
"META-INF/**/*.properties",
"META-INF/README.md", "META-INF/README.md",
"META-INF/NOTICE", "META-INF/NOTICE",
"META-INF/*.kotlin_module", "META-INF/*.version",
), ),
) )
} }
@ -139,6 +143,24 @@ android {
} }
} }
kotlin {
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
dependencies { dependencies {
implementation(projects.i18n) implementation(projects.i18n)
implementation(projects.core.archive) implementation(projects.core.archive)
@ -161,7 +183,6 @@ dependencies {
debugImplementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview) implementation(compose.ui.tooling.preview)
implementation(compose.ui.util) implementation(compose.ui.util)
implementation(compose.accompanist.systemuicontroller)
implementation(androidx.interpolator) implementation(androidx.interpolator)
@ -277,28 +298,6 @@ androidComponents {
} }
} }
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-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=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}
buildscript { buildscript {
dependencies { dependencies {
classpath(kotlinx.gradle) classpath(kotlinx.gradle)

View File

@ -0,0 +1,11 @@
package mihon.core.firebase
import android.content.Context
object FirebaseConfig {
fun init(context: Context) = Unit
fun setAnalyticsEnabled(enabled: Boolean) = Unit
fun setCrashlyticsEnabled(enabled: Boolean) = Unit
}

View File

@ -1,11 +1,12 @@
package eu.kanade.core.util package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
fun <T : R, R : Any> List<T>.insertSeparators( fun <T : R, R : Any> List<T>.insertSeparators(
generator: (T?, T?) -> R?, generator: (before: T?, after: T?) -> R?,
): List<R> { ): List<R> {
if (isEmpty()) return emptyList() if (isEmpty()) return emptyList()
val newList = mutableListOf<R>() val newList = mutableListOf<R>()
@ -19,6 +20,24 @@ fun <T : R, R : Any> List<T>.insertSeparators(
return newList return newList
} }
/**
* Similar to [eu.kanade.core.util.insertSeparators] but iterates from last to first element
*/
fun <T : R, R : Any> List<T>.insertSeparatorsReversed(
generator: (before: T?, after: T?) -> R?,
): List<R> {
if (isEmpty()) return emptyList()
val newList = mutableListOf<R>()
for (i in size downTo 0) {
val after = getOrNull(i)
after?.let(newList::add)
val before = getOrNull(i - 1)
val separator = generator.invoke(before, after)
separator?.let(newList::add)
}
return newList.asReversed()
}
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
if (shouldAdd) { if (shouldAdd) {
add(value) add(value)
@ -27,21 +46,6 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
} }
} }
/**
* Returns a list containing only elements matching the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) }
val destination = ArrayList<T>()
fastForEach { if (predicate(it)) destination.add(it) }
return destination
}
/** /**
* Returns a list containing all elements not matching the given [predicate]. * Returns a list containing all elements not matching the given [predicate].
* *
@ -52,27 +56,7 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
@OptIn(ExperimentalContracts::class) @OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
contract { callsInPlace(predicate) } contract { callsInPlace(predicate) }
val destination = ArrayList<T>() return fastFilter { !predicate(it) }
fastForEach { if (!predicate(it)) destination.add(it) }
return destination
}
/**
* Returns a list containing only the non-null results of applying the
* given [transform] function to each element in the original collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
contract { callsInPlace(transform) }
val destination = ArrayList<R>()
fastForEach { element ->
transform(element)?.let(destination::add)
}
return destination
} }
/** /**
@ -113,26 +97,3 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
fastForEach { if (predicate(it)) --count } fastForEach { if (predicate(it)) --count }
return count return count
} }
/**
* Returns a list containing only elements from the given collection
* having distinct keys returned by the given [selector] function.
*
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
* The elements in the resulting list are in the same order as they were in the source collection.
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@OptIn(ExperimentalContracts::class)
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
contract { callsInPlace(selector) }
val set = HashSet<K>()
val list = ArrayList<T>()
fastForEach {
val key = selector(it)
if (set.add(key)) list.add(it)
}
return list
}

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.base
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -30,4 +31,8 @@ class BasePreferences(
} }
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
} }

View File

@ -5,6 +5,7 @@ import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
import logcat.LogPriority import logcat.LogPriority
@ -14,17 +15,16 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.history.interactor.GetHistory import tachiyomi.domain.history.interactor.GetHistory
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.ZoneOffset import java.time.ZoneOffset
class AddTracks( class AddTracks(
private val getTracks: GetTracks,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
private val trackerManager: TrackerManager,
) { ) {
// TODO: update all trackers based on common data // TODO: update all trackers based on common data
@ -79,7 +79,7 @@ class AddTracks(
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
withIOContext { withIOContext {
getTracks.await(manga.id) trackerManager.loggedInTrackers()
.filterIsInstance<EnhancedTracker>() .filterIsInstance<EnhancedTracker>()
.filter { it.accept(source) } .filter { it.accept(source) }
.forEach { service -> .forEach { service ->
@ -87,11 +87,11 @@ class AddTracks(
service.match(manga)?.let { track -> service.match(manga)?.let { track ->
track.manga_id = manga.id track.manga_id = manga.id
(service as Tracker).bind(track) (service as Tracker).bind(track)
insertTrack.await(track.toDomainTrack()!!) insertTrack.await(track.toDomainTrack(idRequired = false)!!)
syncChapterProgressWithTrack.await( syncChapterProgressWithTrack.await(
manga.id, manga.id,
track.toDomainTrack()!!, track.toDomainTrack(idRequired = false)!!,
service, service,
) )
} }

View File

@ -10,6 +10,7 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.toChapterUpdate import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import kotlin.math.max
class SyncChapterProgressWithTrack( class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
@ -36,7 +37,8 @@ class SyncChapterProgressWithTrack(
// only take into account continuous reading // only take into account continuous reading
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble()) val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble())
val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead)
try { try {
tracker.update(updatedTrack.toDbTrack()) tracker.update(updatedTrack.toDbTrack())

View File

@ -0,0 +1,10 @@
package eu.kanade.domain.track.model
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
enum class AutoTrackState(val titleRes: StringResource) {
ALWAYS(MR.strings.lock_always),
ASK(MR.strings.default_category_summary),
NEVER(MR.strings.lock_never),
}

View File

@ -1,9 +1,11 @@
package eu.kanade.domain.track.service package eu.kanade.domain.track.service
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
class TrackPreferences( class TrackPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
@ -35,4 +37,9 @@ class TrackPreferences(
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
"pref_auto_update_manga_on_mark_read",
AutoTrackState.ALWAYS,
)
} }

View File

@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) { return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext { withIOContext {
value = try { value = try {
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
val appResources = context.packageManager.getResourcesForApplication(appInfo) val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success( Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!! appResources.getDrawableForDensity(appInfo.icon, density, null)!!

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
@ -14,7 +15,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -33,20 +33,13 @@ import tachiyomi.presentation.core.i18n.stringResource
fun TabbedScreen( fun TabbedScreen(
titleRes: StringResource, titleRes: StringResource,
tabs: ImmutableList<TabContent>, tabs: ImmutableList<TabContent>,
startIndex: Int? = null, state: PagerState = rememberPagerState { tabs.size },
searchQuery: String? = null, searchQuery: String? = null,
onChangeSearchQuery: (String?) -> Unit = {}, onChangeSearchQuery: (String?) -> Unit = {},
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = rememberPagerState { tabs.size }
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(startIndex) {
if (startIndex != null) {
state.scrollToPage(startIndex)
}
}
Scaffold( Scaffold(
topBar = { topBar = {
val tab = tabs[state.currentPage] val tab = tabs[state.currentPage]

View File

@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -27,6 +29,7 @@ import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.sort import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.BaseSortItem
import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.SettingsChipRow import tachiyomi.presentation.core.components.SettingsChipRow
@ -163,22 +166,38 @@ private fun ColumnScope.SortPage(
val sortingMode = category.sort.type val sortingMode = category.sort.type
val sortDescending = !category.sort.isAscending val sortDescending = !category.sort.isAscending
val trackerSortOption = if (trackers.isEmpty()) { val options = remember(trackers.isEmpty()) {
emptyList() val trackerMeanPair = if (trackers.isNotEmpty()) {
} else { MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean
listOf(MR.strings.action_sort_tracker_score to LibrarySort.Type.TrackerMean) } else {
null
}
listOfNotNull(
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical,
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters,
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead,
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount,
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
trackerMeanPair,
MR.strings.action_sort_random to LibrarySort.Type.Random,
)
} }
listOf( options.map { (titleRes, mode) ->
MR.strings.action_sort_alpha to LibrarySort.Type.Alphabetical, if (mode == LibrarySort.Type.Random) {
MR.strings.action_sort_total to LibrarySort.Type.TotalChapters, BaseSortItem(
MR.strings.action_sort_last_read to LibrarySort.Type.LastRead, label = stringResource(titleRes),
MR.strings.action_sort_last_manga_update to LibrarySort.Type.LastUpdate, icon = Icons.Default.Refresh
MR.strings.action_sort_unread_count to LibrarySort.Type.UnreadCount, .takeIf { sortingMode == LibrarySort.Type.Random },
MR.strings.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, onClick = {
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, screenModel.setSort(category, mode, LibrarySort.Direction.Ascending)
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded, },
).plus(trackerSortOption).map { (titleRes, mode) -> )
return@map
}
SortItem( SortItem(
label = stringResource(titleRes), label = stringResource(titleRes),
sortDescending = sortDescending.takeIf { sortingMode == mode }, sortDescending = sortDescending.takeIf { sortingMode == mode },

View File

@ -21,11 +21,12 @@ internal fun LibraryTabs(
getNumberOfMangaForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
Column( Column(
modifier = Modifier.zIndex(1f), modifier = Modifier.zIndex(1f),
) { ) {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = currentPageIndex,
edgePadding = 0.dp, edgePadding = 0.dp,
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
@ -33,7 +34,7 @@ internal fun LibraryTabs(
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
Tab( Tab(
selected = pagerState.currentPage == index, selected = currentPageIndex == index,
onClick = { onTabItemClick(index) }, onClick = { onTabItemClick(index) },
text = { text = {
TabText( TabText(

View File

@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@ -34,13 +36,18 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep { internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false) private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false) private var batteryGranted by mutableStateOf(false)
@ -73,7 +80,7 @@ internal class PermissionStep : OnboardingStep {
} }
Column { Column {
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_install_apps), title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted, granted = installGranted,
@ -89,7 +96,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume // no-op. resulting checks is being done on resume
}, },
) )
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_notifications), title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted, granted = notificationGranted,
@ -97,7 +104,7 @@ internal class PermissionStep : OnboardingStep {
) )
} }
PermissionItem( PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted, granted = batteryGranted,
@ -109,6 +116,29 @@ internal class PermissionStep : OnboardingStep {
context.startActivity(intent) context.startActivity(intent)
}, },
) )
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
val crashlyticsPref = privacyPreferences.crashlytics()
val crashlytics by crashlyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
granted = crashlytics,
onToggleChange = crashlyticsPref::set,
)
val analyticsPref = privacyPreferences.analytics()
val analytics by analyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
granted = analytics,
onToggleChange = analyticsPref::set,
)
} }
} }
@ -127,7 +157,7 @@ internal class PermissionStep : OnboardingStep {
} }
@Composable @Composable
private fun PermissionItem( private fun PermissionCheckbox(
title: String, title: String,
subtitle: String, subtitle: String,
granted: Boolean, granted: Boolean,
@ -157,4 +187,26 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
) )
} }
@Composable
private fun PermissionSwitch(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onToggleChange: (Boolean) -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
Switch(
checked = granted,
onCheckedChange = onToggleChange,
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
} }

View File

@ -162,12 +162,12 @@ sealed class Preference {
data class CustomPreference( data class CustomPreference(
override val title: String, override val title: String,
val content: @Composable (PreferenceItem<String>) -> Unit, val content: @Composable () -> Unit,
) : PreferenceItem<String>() { ) : PreferenceItem<Unit>() {
override val enabled: Boolean = true override val enabled: Boolean = true
override val subtitle: String? = null override val subtitle: String? = null
override val icon: ImageVector? = null override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true } override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
} }
} }

View File

@ -167,7 +167,7 @@ internal fun PreferenceItem(
InfoWidget(text = item.title) InfoWidget(text = item.title)
} }
is Preference.PreferenceItem.CustomPreference -> { is Preference.PreferenceItem.CustomPreference -> {
item.content(item) item.content()
} }
} }
} }

View File

@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.isShizukuInstalled
@ -61,6 +62,7 @@ import logcat.LogPriority
import okhttp3.Headers import okhttp3.Headers
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -334,6 +336,31 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader), title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = basePreferences.hardwareBitmapThreshold(),
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
subtitleProvider = { value, options ->
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
},
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
.mapIndexed { index, option ->
val display = if (index == 0) {
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
} else {
option.toString()
}
option to display
}
.toMap()
.toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile), title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(), subtitle = basePreferences.displayProfile().get(),

View File

@ -7,7 +7,9 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -15,6 +17,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@ -23,11 +27,14 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@ -45,10 +52,13 @@ import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.export.LibraryExporter
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath import tachiyomi.core.common.storage.displayablePath
@ -57,8 +67,10 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -95,6 +107,7 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(), getDataGroup(),
getExportGroup(),
) )
} }
@ -312,4 +325,137 @@ object SettingsDataScreen : SearchableSettings {
), ),
) )
} }
@Composable
private fun getExportGroup(): Preference.PreferenceGroup {
var showDialog by remember { mutableStateOf(false) }
var titleSelected by remember { mutableStateOf(true) }
var authorSelected by remember { mutableStateOf(true) }
var artistSelected by remember { mutableStateOf(true) }
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val getFavorites: GetFavorites = Injekt.get()
val favoritesFlow = remember { flow { emit(getFavorites.await()) } }
val favoritesState by favoritesFlow.collectAsState(emptyList())
val libraryExporter = LibraryExporter()
val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/csv"),
) { uri ->
uri?.let {
libraryExporter.exportToCsv(
context,
it,
favoritesState,
LibraryExporter.ExportOptions(titleSelected, authorSelected, artistSelected),
coroutineScope,
) {
context.toast(MR.strings.library_exported)
}
}
}
if (showDialog) {
ColumnSelectionDialog(
onDismissRequest = { showDialog = false },
onConfirm = { newTitleSelected, newAuthorSelected, newArtistSelected ->
titleSelected = newTitleSelected
authorSelected = newAuthorSelected
artistSelected = newArtistSelected
saveFileLauncher.launch("library_list.csv")
},
isTitleSelected = titleSelected,
isAuthorSelected = authorSelected,
isArtistSelected = artistSelected,
)
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.export),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.library_list),
onClick = { showDialog = true },
),
),
)
}
@Composable
private fun ColumnSelectionDialog(
onDismissRequest: () -> Unit,
onConfirm: (Boolean, Boolean, Boolean) -> Unit,
isTitleSelected: Boolean,
isAuthorSelected: Boolean,
isArtistSelected: Boolean,
) {
var titleSelected by remember { mutableStateOf(isTitleSelected) }
var authorSelected by remember { mutableStateOf(isAuthorSelected) }
var artistSelected by remember { mutableStateOf(isArtistSelected) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
},
text = {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = titleSelected,
onCheckedChange = { checked ->
titleSelected = checked
if (!checked) {
authorSelected = false
artistSelected = false
}
},
)
Text(text = stringResource(MR.strings.title))
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = authorSelected,
onCheckedChange = { authorSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.author))
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(
checked = artistSelected,
onCheckedChange = { artistSelected = it },
enabled = titleSelected,
)
Text(text = stringResource(MR.strings.artist))
}
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(titleSelected, authorSelected, artistSelected)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
} }

View File

@ -15,7 +15,6 @@ import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
@ -35,7 +34,7 @@ object SettingsDownloadScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() }) val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf( return listOf(

View File

@ -24,7 +24,6 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags import tachiyomi.domain.category.interactor.ResetCategoryFlags
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -53,7 +52,7 @@ object SettingsLibraryScreen : SearchableSettings {
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val getCategories = remember { Injekt.get<GetCategories>() } val getCategories = remember { Injekt.get<GetCategories>() }
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() } val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() }) val allCategories by getCategories.subscribe().collectAsState(initial = emptyList())
return listOf( return listOf(
getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences), getCategoriesGroup(LocalNavigator.currentOrThrow, allCategories, libraryPreferences),

View File

@ -7,6 +7,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
@ -28,55 +29,91 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() } val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() } val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return listOf(
getSecurityGroup(securityPreferences),
getFirebaseGroup(privacyPreferences),
)
}
@Composable
private fun getSecurityGroup(
securityPreferences: SecurityPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator() val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState() val useAuth by useAuthPref.collectAsState()
return listOf( return Preference.PreferenceGroup(
Preference.PreferenceItem.SwitchPreference( title = stringResource(MR.strings.pref_security),
pref = useAuthPref, preferenceItems = persistentListOf(
title = stringResource(MR.strings.lock_with_biometrics), Preference.PreferenceItem.SwitchPreference(
enabled = authSupported, pref = useAuthPref,
onValueChanged = { title = stringResource(MR.strings.lock_with_biometrics),
(context as FragmentActivity).authenticate( enabled = authSupported,
title = context.stringResource(MR.strings.lock_with_biometrics), onValueChanged = {
) (context as FragmentActivity).authenticate(
}, title = context.stringResource(MR.strings.lock_with_biometrics),
), )
Preference.PreferenceItem.ListPreference( },
pref = securityPreferences.lockAppAfter(), ),
title = stringResource(MR.strings.lock_when_idle), Preference.PreferenceItem.ListPreference(
enabled = authSupported && useAuth, pref = securityPreferences.lockAppAfter(),
entries = LockAfterValues title = stringResource(MR.strings.lock_when_idle),
.associateWith { enabled = authSupported && useAuth,
when (it) { entries = LockAfterValues
-1 -> stringResource(MR.strings.lock_never) .associateWith {
0 -> stringResource(MR.strings.lock_always) when (it) {
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) -1 -> stringResource(MR.strings.lock_never)
0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
}
} }
} .toImmutableMap(),
.toImmutableMap(), onValueChanged = {
onValueChanged = { (context as FragmentActivity).authenticate(
(context as FragmentActivity).authenticate( title = context.stringResource(MR.strings.lock_when_idle),
title = context.stringResource(MR.strings.lock_when_idle), )
) },
}, ),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
), ),
Preference.PreferenceItem.SwitchPreference( )
pref = securityPreferences.hideNotificationContent(), }
title = stringResource(MR.strings.hide_notification_content),
@Composable
private fun getFirebaseGroup(
privacyPreferences: PrivacyPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
), ),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
) )
} }
} }

View File

@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.data.track.EnhancedTracker import eu.kanade.tachiyomi.data.track.EnhancedTracker
@ -53,6 +54,7 @@ import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@ -85,6 +87,7 @@ object SettingsTrackingScreen : SearchableSettings {
val trackPreferences = remember { Injekt.get<TrackPreferences>() } val trackPreferences = remember { Injekt.get<TrackPreferences>() }
val trackerManager = remember { Injekt.get<TrackerManager>() } val trackerManager = remember { Injekt.get<TrackerManager>() }
val sourceManager = remember { Injekt.get<SourceManager>() } val sourceManager = remember { Injekt.get<SourceManager>() }
val autoTrackStatePref = trackPreferences.autoUpdateTrackOnMarkRead()
var dialog by remember { mutableStateOf<Any?>(null) } var dialog by remember { mutableStateOf<Any?>(null) }
dialog?.run { dialog?.run {
@ -125,6 +128,13 @@ object SettingsTrackingScreen : SearchableSettings {
pref = trackPreferences.autoUpdateTrack(), pref = trackPreferences.autoUpdateTrack(),
title = stringResource(MR.strings.pref_auto_update_manga_sync), title = stringResource(MR.strings.pref_auto_update_manga_sync),
), ),
Preference.PreferenceItem.ListPreference(
pref = trackPreferences.autoUpdateTrackOnMarkRead(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
entries = AutoTrackState.entries
.associateWith { stringResource(it.titleRes) }
.toPersistentMap(),
),
Preference.PreferenceGroup( Preference.PreferenceGroup(
title = stringResource(MR.strings.services), title = stringResource(MR.strings.services),
preferenceItems = persistentListOf( preferenceItems = persistentListOf(

View File

@ -78,7 +78,7 @@ class DebugInfoScreen : Screen() {
val status by produceState(initialValue = "-") { val status by produceState(initialValue = "-") {
val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode
value = when (result) { value = when (result) {
ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE_INSTALLED -> "No profile installed"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled"
ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING ->
"Compiled non-matching" "Compiled non-matching"
@ -88,6 +88,7 @@ class DebugInfoScreen : Screen() {
-> "Error $result" -> "Error $result"
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported" ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported"
ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation" ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation"
ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_NO_PROFILE_EMBEDDED -> "No profile embedded"
else -> "Unknown code $result" else -> "Unknown code $result"
} }
} }

View File

@ -45,8 +45,8 @@ fun ReaderAppBars(
onClickTopAppBar: () -> Unit, onClickTopAppBar: () -> Unit,
bookmarked: Boolean, bookmarked: Boolean,
onToggleBookmarked: () -> Unit, onToggleBookmarked: () -> Unit,
onOpenInBrowser: (() -> Unit)?,
onOpenInWebView: (() -> Unit)?, onOpenInWebView: (() -> Unit)?,
onOpenInBrowser: (() -> Unit)?,
onShare: (() -> Unit)?, onShare: (() -> Unit)?,
viewer: Viewer?, viewer: Viewer?,
@ -56,7 +56,7 @@ fun ReaderAppBars(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onSliderValueChange: (Int) -> Unit, onPageIndexChange: (Int) -> Unit,
readingMode: ReadingMode, readingMode: ReadingMode,
onClickReadingMode: () -> Unit, onClickReadingMode: () -> Unit,
@ -120,14 +120,6 @@ fun ReaderAppBars(
onClick = onToggleBookmarked, onClick = onToggleBookmarked,
), ),
) )
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onOpenInWebView?.let { onOpenInWebView?.let {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
@ -136,6 +128,14 @@ fun ReaderAppBars(
), ),
) )
} }
onOpenInBrowser?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_open_in_browser),
onClick = it,
),
)
}
onShare?.let { onShare?.let {
add( add(
AppBar.OverflowAction( AppBar.OverflowAction(
@ -176,9 +176,8 @@ fun ReaderAppBars(
enabledPrevious = enabledPrevious, enabledPrevious = enabledPrevious,
currentPage = currentPage, currentPage = currentPage,
totalPages = totalPages, totalPages = totalPages,
onSliderValueChange = onSliderValueChange, onPageIndexChange = onPageIndexChange,
) )
BottomReaderBar( BottomReaderBar(
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
readingMode = readingMode, readingMode = readingMode,

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -16,7 +17,6 @@ import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@ -38,8 +39,8 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.presentation.util.isTabletUi import eu.kanade.presentation.util.isTabletUi
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Slider
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import kotlin.math.roundToInt
@Composable @Composable
fun ChapterNavigator( fun ChapterNavigator(
@ -50,7 +51,7 @@ fun ChapterNavigator(
enabledPrevious: Boolean, enabledPrevious: Boolean,
currentPage: Int, currentPage: Int,
totalPages: Int, totalPages: Int,
onSliderValueChange: (Int) -> Unit, onPageIndexChange: (Int) -> Unit,
) { ) {
val isTabletUi = isTabletUi() val isTabletUi = isTabletUi()
val horizontalPadding = if (isTabletUi) 24.dp else 8.dp val horizontalPadding = if (isTabletUi) 24.dp else 8.dp
@ -97,7 +98,11 @@ fun ChapterNavigator(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = currentPage.toString()) Box(contentAlignment = Alignment.CenterEnd) {
Text(text = currentPage.toString())
// Taking up full length so the slider doesn't shift when 'currentPage' length changes
Text(text = totalPages.toString(), color = Color.Transparent)
}
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val sliderDragged by interactionSource.collectIsDraggedAsState() val sliderDragged by interactionSource.collectIsDraggedAsState()
@ -110,11 +115,11 @@ fun ChapterNavigator(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = currentPage.toFloat(), value = currentPage,
valueRange = 1f..totalPages.toFloat(), valueRange = 1..totalPages,
steps = totalPages - 2, onValueChange = f@{
onValueChange = { if (it == currentPage) return@f
onSliderValueChange(it.roundToInt() - 1) onPageIndexChange(it - 1)
}, },
interactionSource = interactionSource, interactionSource = interactionSource,
) )
@ -155,7 +160,7 @@ private fun ChapterNavigatorPreview() {
enabledPrevious = true, enabledPrevious = true,
currentPage = currentPage, currentPage = currentPage,
totalPages = 10, totalPages = 10,
onSliderValueChange = { currentPage = it }, onPageIndexChange = { currentPage = (it + 1) },
) )
} }
} }

View File

@ -55,7 +55,10 @@ interface AssistContentScreen {
} }
@Composable @Composable
fun DefaultNavigatorScreenTransition(navigator: Navigator) { fun DefaultNavigatorScreenTransition(
navigator: Navigator,
modifier: Modifier = Modifier,
) {
val slideDistance = rememberSlideDistance() val slideDistance = rememberSlideDistance()
ScreenTransition( ScreenTransition(
navigator = navigator, navigator = navigator,
@ -65,6 +68,7 @@ fun DefaultNavigatorScreenTransition(navigator: Navigator) {
slideDistance = slideDistance, slideDistance = slideDistance,
) )
}, },
modifier = modifier,
) )
} }

View File

@ -26,6 +26,7 @@ import eu.kanade.domain.DomainModule
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
@ -40,6 +41,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
@ -50,12 +52,14 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import mihon.core.firebase.FirebaseConfig
import mihon.core.migration.Migrator import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager import tachiyomi.presentation.widget.WidgetManager
@ -67,6 +71,7 @@ import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy() private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
@ -75,6 +80,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
patchInjekt() patchInjekt()
FirebaseConfig.init(applicationContext)
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
@ -97,6 +103,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
val scope = ProcessLifecycleOwner.get().lifecycleScope
// Show notification to disable Incognito Mode when it's enabled // Show notification to disable Incognito Mode when it's enabled
basePreferences.incognitoMode().changes() basePreferences.incognitoMode().changes()
.onEach { enabled -> .onEach { enabled ->
@ -124,14 +132,30 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
cancelNotification(Notifications.ID_INCOGNITO_MODE) cancelNotification(Notifications.ID_INCOGNITO_MODE)
} }
} }
.launchIn(ProcessLifecycleOwner.get().lifecycleScope) .launchIn(scope)
privacyPreferences.analytics()
.changes()
.onEach(FirebaseConfig::setAnalyticsEnabled)
.launchIn(scope)
privacyPreferences.crashlytics()
.changes()
.onEach(FirebaseConfig::setCrashlyticsEnabled)
.launchIn(scope)
basePreferences.hardwareBitmapThreshold().let { preference ->
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
}
basePreferences.hardwareBitmapThreshold().changes()
.onEach { ImageUtil.hardwareBitmapThreshold = it }
.launchIn(scope)
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get()) setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
// Updates widget update // Updates widget update
with(WidgetManager(Injekt.get(), Injekt.get())) { WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
init(ProcessLifecycleOwner.get().lifecycleScope)
}
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) { if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))

View File

@ -11,7 +11,6 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import kotlin.system.exitProcess
class GlobalExceptionHandler private constructor( class GlobalExceptionHandler private constructor(
private val applicationContext: Context, private val applicationContext: Context,
@ -31,13 +30,9 @@ class GlobalExceptionHandler private constructor(
} }
override fun uncaughtException(thread: Thread, exception: Throwable) { override fun uncaughtException(thread: Thread, exception: Throwable) {
try { logcat(priority = LogPriority.ERROR, throwable = exception)
logcat(priority = LogPriority.ERROR, throwable = exception) launchActivity(applicationContext, activityToBeLaunched, exception)
launchActivity(applicationContext, activityToBeLaunched, exception) defaultHandler.uncaughtException(thread, exception)
exitProcess(0)
} catch (_: Exception) {
defaultHandler.uncaughtException(thread, exception)
}
} }
private fun launchActivity( private fun launchActivity(

View File

@ -27,6 +27,7 @@ import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -43,6 +44,7 @@ class BackupCreator(
private val parser: ProtoBuf = Injekt.get(), private val parser: ProtoBuf = Injekt.get(),
private val getFavorites: GetFavorites = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
private val backupPreferences: BackupPreferences = Injekt.get(), private val backupPreferences: BackupPreferences = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
@ -75,7 +77,9 @@ class BackupCreator(
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error)) throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
} }
val backupManga = backupMangas(getFavorites.await(), options) val nonFavoriteManga = if (options.readEntries) mangaRepository.getReadMangaNotInLibrary() else emptyList()
val backupManga = backupMangas(getFavorites.await() + nonFavoriteManga, options)
val backup = Backup( val backup = Backup(
backupManga = backupManga, backupManga = backupManga,
backupCategories = backupCategories(options), backupCategories = backupCategories(options),

View File

@ -10,6 +10,7 @@ data class BackupOptions(
val chapters: Boolean = true, val chapters: Boolean = true,
val tracking: Boolean = true, val tracking: Boolean = true,
val history: Boolean = true, val history: Boolean = true,
val readEntries: Boolean = true,
val appSettings: Boolean = true, val appSettings: Boolean = true,
val extensionRepoSettings: Boolean = true, val extensionRepoSettings: Boolean = true,
val sourceSettings: Boolean = true, val sourceSettings: Boolean = true,
@ -22,6 +23,7 @@ data class BackupOptions(
chapters, chapters,
tracking, tracking,
history, history,
readEntries,
appSettings, appSettings,
extensionRepoSettings, extensionRepoSettings,
sourceSettings, sourceSettings,
@ -60,6 +62,12 @@ data class BackupOptions(
getter = BackupOptions::categories, getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) }, setter = { options, enabled -> options.copy(categories = enabled) },
), ),
Entry(
label = MR.strings.non_library_settings,
getter = BackupOptions::readEntries,
setter = { options, enabled -> options.copy(readEntries = enabled) },
enabled = { it.libraryEntries },
),
) )
val settingsOptions = persistentListOf( val settingsOptions = persistentListOf(
@ -92,10 +100,11 @@ data class BackupOptions(
chapters = array[2], chapters = array[2],
tracking = array[3], tracking = array[3],
history = array[4], history = array[4],
appSettings = array[5], readEntries = array[5],
extensionRepoSettings = array[6], appSettings = array[6],
sourceSettings = array[7], extensionRepoSettings = array[7],
privateSettings = array[8], sourceSettings = array[8],
privateSettings = array[9],
) )
} }

View File

@ -10,7 +10,6 @@ import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.request.bitmapConfig import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
@ -46,10 +45,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
check(bitmap != null) { "Failed to decode image" } check(bitmap != null) { "Failed to decode image" }
if ( if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
options.bitmapConfig == Bitmap.Config.HARDWARE &&
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) { if (hwBitmap != null) {
bitmap.recycle() bitmap.recycle()

View File

@ -1,4 +1,4 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming") @file:Suppress("PropertyName")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models

View File

@ -1,4 +1,4 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming") @file:Suppress("PropertyName")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models

View File

@ -1,4 +1,4 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming") @file:Suppress("PropertyName")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models

View File

@ -1,4 +1,4 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming") @file:Suppress("PropertyName")
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models

View File

@ -96,13 +96,13 @@ class DownloadCache(
private val diskCacheFile: File private val diskCacheFile: File
get() = File(context.cacheDir, "dl_index_cache_v3") get() = File(context.cacheDir, "dl_index_cache_v3")
private val rootDownloadsDirLock = Mutex() private val rootDownloadsDirMutex = Mutex()
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
init { init {
// Attempt to read cache file // Attempt to read cache file
scope.launch { scope.launch {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
try { try {
if (diskCacheFile.exists()) { if (diskCacheFile.exists()) {
val diskCache = diskCacheFile.inputStream().use { val diskCache = diskCacheFile.inputStream().use {
@ -112,7 +112,7 @@ class DownloadCache(
lastRenew = System.currentTimeMillis() lastRenew = System.currentTimeMillis()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to initialize disk cache" } logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" }
diskCacheFile.delete() diskCacheFile.delete()
} }
} }
@ -198,7 +198,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) { suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
// Retrieve the cached source directory or cache a new one // Retrieve the cached source directory or cache a new one
var sourceDir = rootDownloadsDir.sourceDirs[manga.source] var sourceDir = rootDownloadsDir.sourceDirs[manga.source]
if (sourceDir == null) { if (sourceDir == null) {
@ -230,7 +230,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun removeChapter(chapter: Chapter, manga: Manga) { suspend fun removeChapter(chapter: Chapter, manga: Manga) {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach { provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
@ -250,7 +250,7 @@ class DownloadCache(
* @param manga the manga of the chapter. * @param manga the manga of the chapter.
*/ */
suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) { suspend fun removeChapters(chapters: List<Chapter>, manga: Manga) {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter -> chapters.forEach { chapter ->
@ -271,7 +271,7 @@ class DownloadCache(
* @param manga the manga to remove. * @param manga the manga to remove.
*/ */
suspend fun removeManga(manga: Manga) { suspend fun removeManga(manga: Manga) {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga.title) val mangaDirName = provider.getMangaDirName(manga.title)
if (sourceDir.mangaDirs.containsKey(mangaDirName)) { if (sourceDir.mangaDirs.containsKey(mangaDirName)) {
@ -283,7 +283,7 @@ class DownloadCache(
} }
suspend fun removeSource(source: Source) { suspend fun removeSource(source: Source) {
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
rootDownloadsDir.sourceDirs -= source.id rootDownloadsDir.sourceDirs -= source.id
} }
@ -322,10 +322,10 @@ class DownloadCache(
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirLock.withLock { rootDownloadsDirMutex.withLock {
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) val updatedRootDir = RootDirectory(storageManager.getDownloadsDirectory())
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() updatedRootDir.sourceDirs = updatedRootDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() } .filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir -> .mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()] val sourceId = sourceMap[dir.name!!.lowercase()]
@ -333,36 +333,35 @@ class DownloadCache(
} }
.toMap() .toMap()
rootDownloadsDir.sourceDirs = sourceDirs updatedRootDir.sourceDirs.values.map { sourceDir ->
async {
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDirs.values sourceDir.mangaDirs.values.forEach { mangaDir ->
.map { sourceDir -> val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
async { .mapNotNull {
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() when {
.filter { it.isDirectory && !it.name.isNullOrBlank() } // Ignore incomplete downloads
.associate { it.name!! to MangaDirectory(it) } it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
// Folder of images
sourceDir.mangaDirs.values.forEach { mangaDir -> it.isDirectory -> it.name
val chapterDirs = mangaDir.dir?.listFiles().orEmpty() // CBZ files
.mapNotNull { it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
when { // Anything else is irrelevant
// Ignore incomplete downloads else -> null
it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null
// Folder of images
it.isDirectory -> it.name
// CBZ files
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
// Anything else is irrelevant
else -> null
}
} }
.toMutableSet() }
.toMutableSet()
mangaDir.chapterDirs = chapterDirs mangaDir.chapterDirs = chapterDirs
}
} }
} }
}
.awaitAll() .awaitAll()
rootDownloadsDir = updatedRootDir
} }
_isInitializing.emit(false) _isInitializing.emit(false)

View File

@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.data.export
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.model.Manga
class LibraryExporter {
data class ExportOptions(
val includeTitle: Boolean,
val includeAuthor: Boolean,
val includeArtist: Boolean,
)
fun exportToCsv(
context: Context,
uri: Uri,
favorites: List<Manga>,
options: ExportOptions,
coroutineScope: CoroutineScope,
onExportComplete: () -> Unit,
) {
coroutineScope.launch {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val csvData = generateCsvData(favorites, options)
outputStream.write(csvData.toByteArray())
outputStream.flush()
onExportComplete()
}
}
}
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
val stringBuilder = StringBuilder()
favorites.forEach { manga ->
val row = mutableListOf<String>()
if (options.includeTitle) row.add(escapeCsvField(manga.title))
if (options.includeAuthor) row.add(escapeCsvField(manga.author ?: ""))
if (options.includeArtist) row.add(escapeCsvField(manga.artist ?: ""))
if (row.isNotEmpty()) {
stringBuilder.appendLine(row.joinToString(",") { "\"$it\"" })
}
}
return stringBuilder.toString()
}
private fun escapeCsvField(field: String): String {
return field
.replace("\"", "\"\"")
.replace("\r\n", "\n")
.replace("\r", "\n")
}
}

View File

@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.library
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build import android.os.Build
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.Constraints import androidx.work.Constraints
@ -92,10 +94,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (tags.contains(WORK_NAME_AUTO)) { if (tags.contains(WORK_NAME_AUTO)) {
val preferences = Injekt.get<LibraryPreferences>() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val restrictions = preferences.autoUpdateDeviceRestrictions().get() val preferences = Injekt.get<LibraryPreferences>()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) { val restrictions = preferences.autoUpdateDeviceRestrictions().get()
return Result.retry() if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.retry()
}
} }
// Find a running manual worker. If exists, try again later // Find a running manual worker. If exists, try again later
@ -432,15 +436,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: preferences.autoUpdateInterval().get() val interval = prefInterval ?: preferences.autoUpdateInterval().get()
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.autoUpdateDeviceRestrictions().get() val restrictions = preferences.autoUpdateDeviceRestrictions().get()
val constraints = Constraints( val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED
NetworkType.UNMETERED } else {
} else { NetworkType.CONNECTED
NetworkType.CONNECTED }
}, val networkRequestBuilder = NetworkRequest.Builder()
requiresCharging = DEVICE_CHARGING in restrictions, if (DEVICE_ONLY_ON_WIFI in restrictions) {
requiresBatteryNotLow = true, networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
) }
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
}
val constraints = Constraints.Builder()
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.setRequiresBatteryNotLow(true)
.build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
interval.toLong(), interval.toLong(),

View File

@ -71,6 +71,7 @@ object Notifications {
const val CHANNEL_APP_UPDATE = "app_apk_update_channel" const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
const val ID_APP_UPDATER = 1 const val ID_APP_UPDATER = 1
const val ID_APP_UPDATE_PROMPT = 2 const val ID_APP_UPDATE_PROMPT = 2
const val ID_APP_UPDATE_ERROR = 3
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel" const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
const val ID_UPDATES_TO_EXTS = -401 const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402 const val ID_EXTENSION_INSTALLER = -402

View File

@ -175,6 +175,7 @@ sealed class Image(
} }
sealed interface Location { sealed interface Location {
@ConsistentCopyVisibility
data class Pictures private constructor(val relativePath: String) : Location { data class Pictures private constructor(val relativePath: String) : Location {
companion object { companion object {
fun create(relativePath: String = ""): Pictures { fun create(relativePath: String = ""): Pictures {

View File

@ -9,7 +9,7 @@ data class ALSearchItem(
val coverImage: ItemCover, val coverImage: ItemCover,
val description: String?, val description: String?,
val format: String, val format: String,
val status: String = "", val status: String?,
val startDate: ALFuzzyDate, val startDate: ALFuzzyDate,
val chapters: Long?, val chapters: Long?,
val averageScore: Int?, val averageScore: Int?,
@ -20,7 +20,7 @@ data class ALSearchItem(
imageUrl = coverImage.large, imageUrl = coverImage.large,
description = description, description = description,
format = format.replace("_", "-"), format = format.replace("_", "-"),
publishingStatus = status, publishingStatus = status ?: "",
startDateFuzzy = startDate.toEpochMilli(), startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0, totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1, averageScore = averageScore ?: -1,

View File

@ -71,6 +71,8 @@ class BangumiApi(
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}" val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
.toUri() .toUri()
.buildUpon() .buildUpon()
.appendQueryParameter("type", "1")
.appendQueryParameter("responseGroup", "large")
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
with(json) { with(json) {
@ -81,7 +83,6 @@ class BangumiApi(
if (result.code == 404) emptyList<TrackSearch>() if (result.code == 404) emptyList<TrackSearch>()
result.list result.list
?.filter { it.type == 1 }
?.map { it.toTrackSearch(trackId) } ?.map { it.toTrackSearch(trackId) }
.orEmpty() .orEmpty()
} }

View File

@ -17,6 +17,7 @@ data class BGMSearchItem(
val nameCn: String, val nameCn: String,
val name: String, val name: String,
val type: Int, val type: Int,
val summary: String?,
val images: BGMSearchItemCovers?, val images: BGMSearchItemCovers?,
@SerialName("eps_count") @SerialName("eps_count")
val epsCount: Long?, val epsCount: Long?,
@ -25,9 +26,13 @@ data class BGMSearchItem(
) { ) {
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply { fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
remote_id = this@BGMSearchItem.id remote_id = this@BGMSearchItem.id
title = nameCn title = nameCn.ifBlank { name }
cover_url = images?.common ?: "" cover_url = images?.common.orEmpty()
summary = this@BGMSearchItem.name summary = if (nameCn.isNotBlank()) {
"作品原名:$name" + this@BGMSearchItem.summary?.let { "\n$it" }.orEmpty()
} else {
this@BGMSearchItem.summary.orEmpty()
}
score = rating?.score ?: -1.0 score = rating?.score ?: -1.0
tracking_url = url tracking_url = url
total_chapters = epsCount ?: 0 total_chapters = epsCount ?: 0

View File

@ -1,4 +1,4 @@
@file:Suppress("PropertyName", "ktlint:standard:property-naming") @file:Suppress("PropertyName")
package eu.kanade.tachiyomi.data.track.model package eu.kanade.tachiyomi.data.track.model

View File

@ -181,9 +181,9 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel), context.stringResource(MR.strings.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER), NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
) )
} }
notificationBuilder.show(Notifications.ID_APP_UPDATER) notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
} }
} }

View File

@ -5,6 +5,7 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
@ -39,6 +40,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory { addSingletonFactory {
SecurityPreferences(get()) SecurityPreferences(get())
} }
addSingletonFactory {
PrivacyPreferences(get())
}
addSingletonFactory { addSingletonFactory {
LibraryPreferences(get()) LibraryPreferences(get())
} }

View File

@ -87,7 +87,7 @@ class ExtensionManager(
?: return null ?: return null
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
.loadIcon(context.packageManager) .loadIcon(context.packageManager)
} }
} }

View File

@ -140,7 +140,7 @@ internal object ExtensionLoader {
val path = it.absolutePath val path = it.absolutePath
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS) pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
?.apply { applicationInfo.fixBasePaths(path) } ?.apply { applicationInfo!!.fixBasePaths(path) }
} }
?.filter { isPackageAnExtension(it) } ?.filter { isPackageAnExtension(it) }
?.map { ExtensionInfo(packageInfo = it, isShared = false) } ?.map { ExtensionInfo(packageInfo = it, isShared = false) }
@ -191,7 +191,7 @@ internal object ExtensionLoader {
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS) context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
?.takeIf { isPackageAnExtension(it) } ?.takeIf { isPackageAnExtension(it) }
?.let { ?.let {
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath) it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath)
ExtensionInfo( ExtensionInfo(
packageInfo = it, packageInfo = it,
isShared = false, isShared = false,
@ -226,7 +226,7 @@ internal object ExtensionLoader {
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val pkgManager = context.packageManager val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo val appInfo = pkgInfo.applicationInfo!!
val pkgName = pkgInfo.packageName val pkgName = pkgInfo.packageName
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
@ -365,7 +365,7 @@ internal object ExtensionLoader {
*/ */
private fun getSignatures(pkgInfo: PackageInfo): List<String>? { private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val signingInfo = pkgInfo.signingInfo val signingInfo = pkgInfo.signingInfo!!
if (signingInfo.hasMultipleSigners()) { if (signingInfo.hasMultipleSigners()) {
signingInfo.apkContentsSigners signingInfo.apkContentsSigners
} else { } else {

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse
import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -22,12 +23,14 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.browse.source.sourcesTab import eu.kanade.tachiyomi.ui.browse.source.sourcesTab
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
data class BrowseTab( data object BrowseTab : Tab {
private val toExtensions: Boolean = false,
) : Tab {
override val options: TabOptions override val options: TabOptions
@Composable @Composable
@ -45,6 +48,12 @@ data class BrowseTab(
navigator.push(GlobalSearchScreen()) navigator.push(GlobalSearchScreen())
} }
private val switchToExtensionTabChannel = Channel<Unit>(1, BufferOverflow.DROP_OLDEST)
fun showExtension() {
switchToExtensionTabChannel.trySend(Unit)
}
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
@ -53,17 +62,25 @@ data class BrowseTab(
val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() } val extensionsScreenModel = rememberScreenModel { ExtensionsScreenModel() }
val extensionsState by extensionsScreenModel.state.collectAsState() val extensionsState by extensionsScreenModel.state.collectAsState()
val tabs = persistentListOf(
sourcesTab(),
extensionsTab(extensionsScreenModel),
migrateSourceTab(),
)
val state = rememberPagerState { tabs.size }
TabbedScreen( TabbedScreen(
titleRes = MR.strings.browse, titleRes = MR.strings.browse,
tabs = persistentListOf( tabs = tabs,
sourcesTab(), state = state,
extensionsTab(extensionsScreenModel),
migrateSourceTab(),
),
startIndex = 1.takeIf { toExtensions },
searchQuery = extensionsState.searchQuery, searchQuery = extensionsState.searchQuery,
onChangeSearchQuery = extensionsScreenModel::search, onChangeSearchQuery = extensionsScreenModel::search,
) )
LaunchedEffect(Unit) {
switchToExtensionTabChannel.receiveAsFlow()
.collectLatest { state.scrollToPage(1) }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true (context as? MainActivity)?.ready = true

View File

@ -1,8 +1,15 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.ExtensionScreen import eu.kanade.presentation.browse.ExtensionScreen
@ -12,6 +19,7 @@ import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -21,7 +29,10 @@ fun extensionsTab(
extensionsScreenModel: ExtensionsScreenModel, extensionsScreenModel: ExtensionsScreenModel,
): TabContent { ): TabContent {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
val state by extensionsScreenModel.state.collectAsState() val state by extensionsScreenModel.state.collectAsState()
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
return TabContent( return TabContent(
titleRes = MR.strings.label_extensions, titleRes = MR.strings.label_extensions,
@ -45,7 +56,13 @@ fun extensionsTab(
onLongClickItem = { extension -> onLongClickItem = { extension ->
when (extension) { when (extension) {
is Extension.Available -> extensionsScreenModel.installExtension(extension) is Extension.Available -> extensionsScreenModel.installExtension(extension)
else -> extensionsScreenModel.uninstallExtension(extension) else -> {
if (context.isPackageInstalled(extension.pkgName)) {
extensionsScreenModel.uninstallExtension(extension)
} else {
privateExtensionToUninstall = extension
}
}
} }
}, },
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension, onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
@ -68,6 +85,50 @@ fun extensionsTab(
onUpdateExtension = extensionsScreenModel::updateExtension, onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions, onRefresh = extensionsScreenModel::findAvailableExtensions,
) )
privateExtensionToUninstall?.let { extension ->
ExtensionUninstallConfirmation(
extensionName = extension.name,
onClickConfirm = {
extensionsScreenModel.uninstallExtension(extension)
},
onDismissRequest = {
privateExtensionToUninstall = null
},
)
}
}, },
) )
} }
@Composable
private fun ExtensionUninstallConfirmation(
extensionName: String,
onClickConfirm: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(MR.strings.ext_confirm_remove))
},
text = {
Text(text = stringResource(MR.strings.remove_private_extension_message, extensionName))
},
confirmButton = {
TextButton(
onClick = {
onClickConfirm()
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.ext_remove))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
onDismissRequest = onDismissRequest,
)
}

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@ -29,6 +28,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@ -81,13 +81,12 @@ data class SourceSearchScreen(
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues -> ) { paddingValues ->
val pagingFlow by screenModel.mangaPagerFlowFlow.collectAsState()
val openMigrateDialog: (Manga) -> Unit = { val openMigrateDialog: (Manga) -> Unit = {
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(newManga = it, oldManga = oldManga)) screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(newManga = it, oldManga = oldManga))
} }
BrowseSourceContent( BrowseSourceContent(
source = screenModel.source, source = screenModel.source,
mangaList = pagingFlow.collectAsLazyPagingItems(), mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation), columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode, displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,

View File

@ -31,7 +31,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@ -56,6 +55,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
@ -206,11 +206,9 @@ data class BrowseSourceScreen(
}, },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues -> ) { paddingValues ->
val pagingFlow by screenModel.mangaPagerFlowFlow.collectAsState()
BrowseSourceContent( BrowseSourceContent(
source = screenModel.source, source = screenModel.source,
mangaList = pagingFlow.collectAsLazyPagingItems(), mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation), columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode, displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,

View File

@ -32,7 +32,7 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
object HistoryTab : Tab { data object HistoryTab : Tab {
private val snackbarHostState = SnackbarHostState() private val snackbarHostState = SnackbarHostState()

View File

@ -69,11 +69,11 @@ object HomeScreen : Screen() {
private const val TAB_FADE_DURATION = 200 private const val TAB_FADE_DURATION = 200
private const val TAB_NAVIGATOR_KEY = "HomeTabs" private const val TAB_NAVIGATOR_KEY = "HomeTabs"
private val tabs = listOf( private val TABS = listOf(
LibraryTab, LibraryTab,
UpdatesTab, UpdatesTab,
HistoryTab, HistoryTab,
BrowseTab(), BrowseTab,
MoreTab, MoreTab,
) )
@ -90,7 +90,7 @@ object HomeScreen : Screen() {
startBar = { startBar = {
if (isTabletUi()) { if (isTabletUi()) {
NavigationRail { NavigationRail {
tabs.fastForEach { TABS.fastForEach {
NavigationRailItem(it) NavigationRailItem(it)
} }
} }
@ -107,7 +107,7 @@ object HomeScreen : Screen() {
exit = shrinkVertically(), exit = shrinkVertically(),
) { ) {
NavigationBar { NavigationBar {
tabs.fastForEach { TABS.fastForEach {
NavigationBarItem(it) NavigationBarItem(it)
} }
} }
@ -159,7 +159,12 @@ object HomeScreen : Screen() {
is Tab.Library -> LibraryTab is Tab.Library -> LibraryTab
Tab.Updates -> UpdatesTab Tab.Updates -> UpdatesTab
Tab.History -> HistoryTab Tab.History -> HistoryTab
is Tab.Browse -> BrowseTab(it.toExtensions) is Tab.Browse -> {
if (it.toExtensions) {
BrowseTab.showExtension()
}
BrowseTab
}
is Tab.More -> MoreTab is Tab.More -> MoreTab
} }

View File

@ -4,15 +4,15 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastDistinctBy
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapNotNull
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.PreferenceMutableState import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.core.util.fastFilter
import eu.kanade.core.util.fastFilterNot import eu.kanade.core.util.fastFilterNot
import eu.kanade.core.util.fastMapNotNull
import eu.kanade.core.util.fastPartition import eu.kanade.core.util.fastPartition
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SetReadStatus
@ -72,7 +72,7 @@ import tachiyomi.domain.track.model.Track
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Collections import kotlin.random.Random
/** /**
* Typealias for the library manga, using the category as keys, and list of manga as values. * Typealias for the library manga, using the category as keys, and list of manga as values.
@ -107,16 +107,14 @@ class LibraryScreenModel(
getTracksPerManga.subscribe(), getTracksPerManga.subscribe(),
getTrackingFilterFlow(), getTrackingFilterFlow(),
downloadCache.changes, downloadCache.changes,
) { searchQuery, library, tracks, trackingFiler, _ -> ) { searchQuery, library, tracks, trackingFilter, _ ->
library library
.applyFilters(tracks, trackingFiler) .applyFilters(tracks, trackingFilter)
.applySort(tracks, trackingFiler.keys) .applySort(tracks, trackingFilter.keys)
.mapValues { (_, value) -> .mapValues { (_, value) ->
if (searchQuery != null) { if (searchQuery != null) {
// Filter query
value.filter { it.matches(searchQuery) } value.filter { it.matches(searchQuery) }
} else { } else {
// Don't do anything
value value
} }
} }
@ -171,12 +169,9 @@ class LibraryScreenModel(
.launchIn(screenModelScope) .launchIn(screenModelScope)
} }
/**
* Applies library filters to the given map of manga.
*/
private suspend fun LibraryMap.applyFilters( private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
trackingFiler: Map<Long, TriState>, trackingFilter: Map<Long, TriState>,
): LibraryMap { ): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first() val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded val downloadedOnly = prefs.globalFilterDownloaded
@ -188,10 +183,10 @@ class LibraryScreenModel(
val filterCompleted = prefs.filterCompleted val filterCompleted = prefs.filterCompleted
val filterIntervalCustom = prefs.filterIntervalCustom val filterIntervalCustom = prefs.filterIntervalCustom
val isNotLoggedInAnyTrack = trackingFiler.isEmpty() val isNotLoggedInAnyTrack = trackingFilter.isEmpty()
val excludedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null } val excludedTracks = trackingFilter.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = trackingFiler.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null } val includedTracks = trackingFilter.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = { val filterFnDownloaded: (LibraryItem) -> Boolean = {
@ -249,17 +244,10 @@ class LibraryScreenModel(
filterFnTracking(it) filterFnTracking(it)
} }
return this.mapValues { entry -> entry.value.fastFilter(filterFn) } return mapValues { (_, value) -> value.fastFilter(filterFn) }
} }
/** private fun LibraryMap.applySort(trackMap: Map<Long, List<Track>>, loggedInTrackerIds: Set<Long>): LibraryMap {
* Applies library sorting to the given map of manga.
*/
private fun LibraryMap.applySort(
// Map<MangaId, List<Track>>
trackMap: Map<Long, List<Track>>,
loggedInTrackerIds: Set<Long>,
): LibraryMap {
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase())
} }
@ -278,9 +266,8 @@ class LibraryScreenModel(
} }
} }
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> fun LibrarySort.comparator(): Comparator<LibraryItem> = Comparator { i1, i2 ->
val sort = keys.find { it.id == i1.libraryManga.category }!!.sort when (this.type) {
when (sort.type) {
LibrarySort.Type.Alphabetical -> { LibrarySort.Type.Alphabetical -> {
sortAlphabetically(i1, i2) sortAlphabetically(i1, i2)
} }
@ -293,8 +280,8 @@ class LibraryScreenModel(
LibrarySort.Type.UnreadCount -> when { LibrarySort.Type.UnreadCount -> when {
// Ensure unread content comes first // Ensure unread content comes first
i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0 i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0
i1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1 i1.libraryManga.unreadCount == 0L -> if (this.isAscending) 1 else -1
i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1 i2.libraryManga.unreadCount == 0L -> if (this.isAscending) -1 else 1
else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount) else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount)
} }
LibrarySort.Type.TotalChapters -> { LibrarySort.Type.TotalChapters -> {
@ -314,17 +301,22 @@ class LibraryScreenModel(
val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue
item1Score.compareTo(item2Score) item1Score.compareTo(item2Score)
} }
LibrarySort.Type.Random -> {
error("Why Are We Still Here? Just To Suffer?")
}
} }
} }
return this.mapValues { entry -> return mapValues { (key, value) ->
val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) { if (key.sort.type == LibrarySort.Type.Random) {
Comparator(sortFn) return@mapValues value.shuffled(Random(libraryPreferences.randomSortSeed().get()))
} else {
Collections.reverseOrder(sortFn)
} }
entry.value.sortedWith(comparator.thenComparator(sortAlphabetically)) val comparator = key.sort.comparator()
.let { if (key.sort.isAscending) it else it.reversed() }
.thenComparator(sortAlphabetically)
value.sortedWith(comparator)
} }
} }

View File

@ -61,7 +61,7 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
object LibraryTab : Tab { data object LibraryTab : Tab {
override val options: TabOptions override val options: TabOptions
@Composable @Composable

View File

@ -10,20 +10,25 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -31,16 +36,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.util.Consumer import androidx.core.util.Consumer
import androidx.core.view.WindowCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -48,7 +53,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
@ -97,7 +101,6 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import androidx.compose.ui.graphics.Color.Companion as ComposeColor
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
@ -132,17 +135,13 @@ class MainActivity : BaseActivity() {
return return
} }
// Draw edge-to-edge
// TODO: replace with ComponentActivity#enableEdgeToEdge
WindowCompat.setDecorFitsSystemWindows(window, false)
setComposeContent { setComposeContent {
val context = LocalContext.current
val incognito by preferences.incognitoMode().collectAsState() val incognito by preferences.incognitoMode().collectAsState()
val downloadOnly by preferences.downloadedOnly().collectAsState() val downloadOnly by preferences.downloadedOnly().collectAsState()
val indexing by downloadCache.isInitializing.collectAsState() val indexing by downloadCache.isInitializing.collectAsState()
// Set status bar color considering the top app state banner
val systemUiController = rememberSystemUiController()
val isSystemInDarkTheme = isSystemInDarkTheme() val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when { val statusBarBackgroundColor = when {
indexing -> IndexingBannerBackgroundColor indexing -> IndexingBannerBackgroundColor
@ -150,27 +149,13 @@ class MainActivity : BaseActivity() {
incognito -> IncognitoModeBannerBackgroundColor incognito -> IncognitoModeBannerBackgroundColor
else -> MaterialTheme.colorScheme.surface else -> MaterialTheme.colorScheme.surface
} }
LaunchedEffect(systemUiController, statusBarBackgroundColor) { LaunchedEffect(isSystemInDarkTheme, statusBarBackgroundColor) {
systemUiController.setStatusBarColor( // Draw edge-to-edge and set system bars color to transparent
color = ComposeColor.Transparent, val lightStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK)
darkIcons = statusBarBackgroundColor.luminance() > 0.5, val darkStyle = SystemBarStyle.dark(Color.TRANSPARENT)
transformColorForLightContent = { ComposeColor.Black }, enableEdgeToEdge(
) statusBarStyle = if (statusBarBackgroundColor.luminance() > 0.5) lightStyle else darkStyle,
} navigationBarStyle = if (isSystemInDarkTheme) darkStyle else lightStyle,
// Set navigation bar color
val context = LocalContext.current
val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) {
systemUiController.setNavigationBarColor(
color = if (context.isNavigationBarNeedsScrim()) {
navbarScrimColor.copy(alpha = 0.7f)
} else {
ComposeColor.Transparent
},
darkIcons = !isSystemInDarkTheme,
navigationBarContrastEnforced = false,
transformColorForLightContent = { ComposeColor.Black },
) )
} }
@ -203,13 +188,26 @@ class MainActivity : BaseActivity() {
contentWindowInsets = scaffoldInsets, contentWindowInsets = scaffoldInsets,
) { contentPadding -> ) { contentPadding ->
// Consume insets already used by app state banners // Consume insets already used by app state banners
Box( Box {
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding),
) {
// Shows current screen // Shows current screen
DefaultNavigatorScreenTransition(navigator = navigator) DefaultNavigatorScreenTransition(
navigator = navigator,
modifier = Modifier
.padding(contentPadding)
.consumeWindowInsets(contentPadding),
)
// Draw navigation bar scrim when needed
if (remember { isNavigationBarNeedsScrim() }) {
Spacer(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.windowInsetsBottomHeight(WindowInsets.navigationBars)
.alpha(0.8f)
.background(MaterialTheme.colorScheme.surfaceContainer),
)
}
} }
} }
@ -280,12 +278,13 @@ class MainActivity : BaseActivity() {
@Composable @Composable
private fun HandleOnNewIntent(context: Context, navigator: Navigator) { private fun HandleOnNewIntent(context: Context, navigator: Navigator) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
callbackFlow<Intent> { callbackFlow {
val componentActivity = context as ComponentActivity val componentActivity = context as ComponentActivity
val consumer = Consumer<Intent> { trySend(it) } val consumer = Consumer<Intent> { trySend(it) }
componentActivity.addOnNewIntentListener(consumer) componentActivity.addOnNewIntentListener(consumer)
awaitClose { componentActivity.removeOnNewIntentListener(consumer) } awaitClose { componentActivity.removeOnNewIntentListener(consumer) }
}.collectLatest { handleIntentAction(it, navigator) } }
.collectLatest { handleIntentAction(it, navigator) }
} }
} }
@ -341,6 +340,7 @@ class MainActivity : BaseActivity() {
* When custom animation is used, status and navigation bar color will be set to transparent and will be restored * When custom animation is used, status and navigation bar color will be set to transparent and will be restored
* after the animation is finished. * after the animation is finished.
*/ */
@Suppress("Deprecation")
private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) { private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
val root = findViewById<View>(android.R.id.content) val root = findViewById<View>(android.R.id.content)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && splashScreen != null) {

View File

@ -25,6 +25,8 @@ import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.TrackChapter import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.model.AutoTrackState
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.manga.DownloadAction import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.util.formattedMessage import eu.kanade.presentation.util.formattedMessage
@ -38,6 +40,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -92,6 +95,7 @@ class MangaScreenModel(
private val mangaId: Long, private val mangaId: Long,
private val isFromSource: Boolean, private val isFromSource: Boolean,
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(), readerPreferences: ReaderPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(), private val trackerManager: TrackerManager = Injekt.get(),
private val trackChapter: TrackChapter = Injekt.get(), private val trackChapter: TrackChapter = Injekt.get(),
@ -137,6 +141,7 @@ class MangaScreenModel(
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get() val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get() val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
var autoTrackState = trackPreferences.autoUpdateTrackOnMarkRead().get()
private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope) private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
@ -725,19 +730,29 @@ class MangaScreenModel(
*/ */
fun markChaptersRead(chapters: List<Chapter>, read: Boolean) { fun markChaptersRead(chapters: List<Chapter>, read: Boolean) {
toggleAllSelection(false) toggleAllSelection(false)
if (chapters.isEmpty()) return
screenModelScope.launchIO { screenModelScope.launchIO {
setReadStatus.await( setReadStatus.await(
read = read, read = read,
chapters = chapters.toTypedArray(), chapters = chapters.toTypedArray(),
) )
if (!read) return@launchIO if (!read || successState?.hasLoggedInTrackers == false || autoTrackState == AutoTrackState.NEVER) {
return@launchIO
}
val tracks = getTracks.await(mangaId) val tracks = getTracks.await(mangaId)
val maxChapterNumber = chapters.maxOf { it.chapterNumber } val maxChapterNumber = chapters.maxOf { it.chapterNumber }
val shouldPromptTrackingUpdate = tracks.any { track -> maxChapterNumber > track.lastChapterRead } val shouldPromptTrackingUpdate = tracks.any { track -> maxChapterNumber > track.lastChapterRead }
if (!shouldPromptTrackingUpdate) return@launchIO if (!shouldPromptTrackingUpdate) return@launchIO
if (autoTrackState == AutoTrackState.ALWAYS) {
trackChapter.await(context, mangaId, maxChapterNumber)
withUIContext {
context.toast(context.stringResource(MR.strings.trackers_updated_summary, maxChapterNumber.toInt()))
}
return@launchIO
}
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = context.stringResource(MR.strings.confirm_tracker_update, maxChapterNumber.toInt()), message = context.stringResource(MR.strings.confirm_tracker_update, maxChapterNumber.toInt()),

View File

@ -824,7 +824,11 @@ private data class TrackerRemoveScreen(
fun deleteMangaFromService() { fun deleteMangaFromService() {
screenModelScope.launchNonCancellable { screenModelScope.launchNonCancellable {
(tracker as DeletableTracker).delete(track) try {
(tracker as DeletableTracker).delete(track)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to delete entry from service" }
}
} }
} }

View File

@ -38,7 +38,7 @@ import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
object MoreTab : Tab { data object MoreTab : Tab {
override val options: TabOptions override val options: TabOptions
@Composable @Composable

View File

@ -390,8 +390,8 @@ class ReaderActivity : BaseActivity() {
onClickTopAppBar = ::openMangaScreen, onClickTopAppBar = ::openMangaScreen,
bookmarked = state.bookmarked, bookmarked = state.bookmarked,
onToggleBookmarked = viewModel::toggleChapterBookmark, onToggleBookmarked = viewModel::toggleChapterBookmark,
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource }, onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource },
onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource },
onShare = ::shareChapter.takeIf { isHttpSource }, onShare = ::shareChapter.takeIf { isHttpSource },
viewer = state.viewer, viewer = state.viewer,
@ -401,7 +401,7 @@ class ReaderActivity : BaseActivity() {
enabledPrevious = state.viewerChapters?.prevChapter != null, enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage, currentPage = state.currentPage,
totalPages = state.totalPages, totalPages = state.totalPages,
onSliderValueChange = { onPageIndexChange = {
isScrollingThroughPages = true isScrollingThroughPages = true
moveToPageIndex(it) moveToPageIndex(it)
}, },
@ -565,12 +565,6 @@ class ReaderActivity : BaseActivity() {
} }
} }
private fun openChapterInBrowser() {
assistUrl?.let {
openInBrowser(it.toUri(), forceDefaultBrowser = false)
}
}
private fun openChapterInWebView() { private fun openChapterInWebView() {
val manga = viewModel.manga ?: return val manga = viewModel.manga ?: return
val source = viewModel.getSource() ?: return val source = viewModel.getSource() ?: return
@ -580,6 +574,12 @@ class ReaderActivity : BaseActivity() {
} }
} }
private fun openChapterInBrowser() {
assistUrl?.let {
openInBrowser(it.toUri(), forceDefaultBrowser = false)
}
}
private fun shareChapter() { private fun shareChapter() {
assistUrl?.let { assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain") val intent = it.toUri().toShareIntent(this, type = "text/plain")

View File

@ -33,13 +33,16 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.coil.cropBorders import eu.kanade.tachiyomi.data.coil.cropBorders
import eu.kanade.tachiyomi.data.coil.customDecoder import eu.kanade.tachiyomi.data.coil.customDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* A wrapper view for showing page image. * A wrapper view for showing page image.
@ -57,6 +60,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
private val isWebtoon: Boolean = false, private val isWebtoon: Boolean = false,
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { ) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
private val alwaysDecodeLongStripWithSSIV by lazy {
Injekt.get<BasePreferences>().alwaysDecodeLongStripWithSSIV().get()
}
private var pageView: View? = null private var pageView: View? = null
private var config: Config? = null private var config: Config? = null
@ -233,7 +240,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} else { } else {
SubsamplingScaleImageView(context) SubsamplingScaleImageView(context)
}.apply { }.apply {
setMaxTileSize(GLUtil.maxTextureSize) setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumTileDpi(180) setMinimumTileDpi(180)
@ -288,35 +295,44 @@ open class ReaderPageImageView @JvmOverloads constructor(
}, },
) )
if (isWebtoon) { when (data) {
val request = ImageRequest.Builder(context) is BitmapDrawable -> {
.data(data) setImage(ImageSource.bitmap(data.bitmap))
.memoryCachePolicy(CachePolicy.DISABLED) isVisible = true
.diskCachePolicy(CachePolicy.DISABLED) }
.target( is BufferedSource -> {
onSuccess = { result -> if (!isWebtoon || alwaysDecodeLongStripWithSSIV) {
val image = result as BitmapImage setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
setImage(ImageSource.bitmap(image.bitmap)) setImage(ImageSource.inputStream(data.inputStream()))
isVisible = true isVisible = true
}, return@apply
onError = { }
this@ReaderPageImageView.onImageLoadError()
}, ImageRequest.Builder(context)
) .data(data)
.size(ViewSizeResolver(this@ReaderPageImageView)) .memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.INEXACT) .diskCachePolicy(CachePolicy.DISABLED)
.cropBorders(config.cropBorders) .target(
.customDecoder(true) onSuccess = { result ->
.crossfade(false) val image = result as BitmapImage
.build() setImage(ImageSource.bitmap(image.bitmap))
context.imageLoader.enqueue(request) isVisible = true
} else { },
when (data) { onError = {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap)) onImageLoadError()
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream())) },
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}") )
.size(ViewSizeResolver(this@ReaderPageImageView))
.precision(Precision.INEXACT)
.cropBorders(config.cropBorders)
.customDecoder(true)
.crossfade(false)
.build()
.let(context.imageLoader::enqueue)
}
else -> {
throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
} }
isVisible = true
} }
} }

View File

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.ui.stats package eu.kanade.tachiyomi.ui.stats
import androidx.compose.ui.util.fastDistinctBy
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastMapNotNull
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.fastCountNot import eu.kanade.core.util.fastCountNot
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.core.util.fastFilter
import eu.kanade.core.util.fastFilterNot import eu.kanade.core.util.fastFilterNot
import eu.kanade.core.util.fastMapNotNull
import eu.kanade.presentation.more.stats.StatsScreenState import eu.kanade.presentation.more.stats.StatsScreenState
import eu.kanade.presentation.more.stats.data.StatsData import eu.kanade.presentation.more.stats.data.StatsData
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager

View File

@ -31,7 +31,7 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
object UpdatesTab : Tab { data object UpdatesTab : Tab {
override val options: TabOptions override val options: TabOptions
@Composable @Composable

View File

@ -15,6 +15,7 @@ import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
@ -107,9 +108,13 @@ fun Context.createFileInCacheDir(name: String): File {
fun Context.createReaderThemeContext(): Context { fun Context.createReaderThemeContext(): Context {
val preferences = Injekt.get<UiPreferences>() val preferences = Injekt.get<UiPreferences>()
val readerPreferences = Injekt.get<ReaderPreferences>() val readerPreferences = Injekt.get<ReaderPreferences>()
val themeMode = preferences.themeMode().get()
val isDarkBackground = when (readerPreferences.readerTheme().get()) { val isDarkBackground = when (readerPreferences.readerTheme().get()) {
1, 2 -> true // Black, Gray 1, 2 -> true // Black, Gray
3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default 3 -> when (themeMode) { // Automatic bg uses activity background by default
ThemeMode.SYSTEM -> applicationContext.isNightMode()
else -> themeMode == ThemeMode.DARK
}
else -> false // White else -> false // White
} }
val expected = if (isDarkBackground) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO val expected = if (isDarkBackground) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO

View File

@ -1,18 +1,25 @@
package mihon.feature.upcoming package mihon.feature.upcoming
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.Badge
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
@ -27,9 +34,9 @@ import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.TwoPanelBox import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import java.time.LocalDate import java.time.LocalDate
import java.time.YearMonth import java.time.YearMonth
@ -99,6 +106,33 @@ private fun UpcomingToolbar() {
) )
} }
@Composable
private fun DateHeading(
date: LocalDate,
mangaCount: Int,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = relativeDateText(date),
modifier = Modifier
.padding(MaterialTheme.padding.small)
.padding(start = MaterialTheme.padding.small),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyMedium,
)
Badge(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
) {
Text("$mangaCount")
}
}
}
@Composable @Composable
private fun UpcomingScreenSmallImpl( private fun UpcomingScreenSmallImpl(
listState: LazyListState, listState: LazyListState,
@ -140,7 +174,10 @@ private fun UpcomingScreenSmallImpl(
) )
} }
is UpcomingUIModel.Header -> { is UpcomingUIModel.Header -> {
ListGroupHeader(text = relativeDateText(item.date)) DateHeading(
date = item.date,
mangaCount = item.mangaCount,
)
} }
} }
} }
@ -188,7 +225,10 @@ private fun UpcomingScreenLargeImpl(
) )
} }
is UpcomingUIModel.Header -> { is UpcomingUIModel.Header -> {
ListGroupHeader(text = relativeDateText(item.date)) DateHeading(
date = item.date,
mangaCount = item.mangaCount,
)
} }
} }
} }

View File

@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMapIndexedNotNull import androidx.compose.ui.util.fastMapIndexedNotNull
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.util.insertSeparators import eu.kanade.core.util.insertSeparatorsReversed
import eu.kanade.tachiyomi.util.lang.toLocalDate import eu.kanade.tachiyomi.util.lang.toLocalDate
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
@ -33,7 +33,7 @@ class UpcomingScreenModel(
val upcomingItems = it.toUpcomingUIModels() val upcomingItems = it.toUpcomingUIModels()
state.copy( state.copy(
items = upcomingItems, items = upcomingItems,
events = it.toEvents(), events = upcomingItems.toEvents(),
headerIndexes = upcomingItems.getHeaderIndexes(), headerIndexes = upcomingItems.getHeaderIndexes(),
) )
} }
@ -42,13 +42,16 @@ class UpcomingScreenModel(
} }
private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> { private fun List<Manga>.toUpcomingUIModels(): ImmutableList<UpcomingUIModel> {
var mangaCount = 0
return fastMap { UpcomingUIModel.Item(it) } return fastMap { UpcomingUIModel.Item(it) }
.insertSeparators { before, after -> .insertSeparatorsReversed { before, after ->
if (after != null) mangaCount++
val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate() val beforeDate = before?.manga?.expectedNextUpdate?.toLocalDate()
val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate() val afterDate = after?.manga?.expectedNextUpdate?.toLocalDate()
if (beforeDate != afterDate && afterDate != null) { if (beforeDate != afterDate && afterDate != null) {
UpcomingUIModel.Header(afterDate) UpcomingUIModel.Header(afterDate, mangaCount).also { mangaCount = 0 }
} else { } else {
null null
} }
@ -56,9 +59,9 @@ class UpcomingScreenModel(
.toImmutableList() .toImmutableList()
} }
private fun List<Manga>.toEvents(): ImmutableMap<LocalDate, Int> { private fun List<UpcomingUIModel>.toEvents(): ImmutableMap<LocalDate, Int> {
return groupBy { it.expectedNextUpdate?.toLocalDate() ?: LocalDate.MAX } return filterIsInstance<UpcomingUIModel.Header>()
.mapValues { it.value.size } .associate { it.date to it.mangaCount }
.toImmutableMap() .toImmutableMap()
} }

View File

@ -4,6 +4,6 @@ import tachiyomi.domain.manga.model.Manga
import java.time.LocalDate import java.time.LocalDate
sealed interface UpcomingUIModel { sealed interface UpcomingUIModel {
data class Header(val date: LocalDate) : UpcomingUIModel data class Header(val date: LocalDate, val mangaCount: Int) : UpcomingUIModel
data class Item(val manga: Manga) : UpcomingUIModel data class Item(val manga: Manga) : UpcomingUIModel
} }

View File

@ -20,6 +20,15 @@
tools:node="remove" /> tools:node="remove" />
<application> <application>
<!-- Disable for manual opt-in -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable unnecessary stuff from Firebase --> <!-- Disable unnecessary stuff from Firebase -->
<meta-data <meta-data
android:name="google_analytics_adid_collection_enabled" android:name="google_analytics_adid_collection_enabled"

View File

@ -0,0 +1,25 @@
package mihon.core.firebase
import android.content.Context
import com.google.firebase.FirebaseApp
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
object FirebaseConfig {
private lateinit var analytics: FirebaseAnalytics
private lateinit var crashlytics: FirebaseCrashlytics
fun init(context: Context) {
analytics = FirebaseAnalytics.getInstance(context)
FirebaseApp.initializeApp(context)
crashlytics = FirebaseCrashlytics.getInstance()
}
fun setAnalyticsEnabled(enabled: Boolean) {
analytics.setAnalyticsCollectionEnabled(enabled)
}
fun setCrashlyticsEnabled(enabled: Boolean) {
crashlytics.isCrashlyticsCollectionEnabled = enabled
}
}

1
buildSrc/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -6,6 +6,18 @@ plugins {
val libs = the<LibrariesForLibs>() val libs = the<LibrariesForLibs>()
val xmlFormatExclude = buildList(2) {
add("**/build/**/*.xml")
projectDir
.resolve("src/commonMain/moko-resources")
.takeIf { it.isDirectory }
?.let(::fileTree)
?.matching { exclude("/base/**") }
?.let(::add)
}
.toTypedArray()
spotless { spotless {
kotlin { kotlin {
target("**/*.kt", "**/*.kts") target("**/*.kt", "**/*.kts")
@ -23,7 +35,7 @@ spotless {
} }
format("xml") { format("xml") {
target("**/*.xml") target("**/*.xml")
targetExclude("**/build/**/*.xml") targetExclude(*xmlFormatExclude)
trimTrailingWhitespace() trimTrailingWhitespace()
endWithNewline() endWithNewline()
} }

View File

@ -4,7 +4,7 @@ import org.gradle.api.JavaVersion as GradleJavaVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget
object AndroidConfig { object AndroidConfig {
const val COMPILE_SDK = 34 const val COMPILE_SDK = 35
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val NDK = "27.1.12297006" const val NDK = "27.1.12297006"

View File

@ -45,8 +45,8 @@ internal fun Project.configureAndroid(commonExtension: CommonExtension<*, *, *,
compilerOptions { compilerOptions {
jvmTarget.set(AndroidConfig.JvmTarget) jvmTarget.set(AndroidConfig.JvmTarget)
freeCompilerArgs.addAll( freeCompilerArgs.addAll(
"-opt-in=kotlin.RequiresOptIn",
"-Xcontext-receivers", "-Xcontext-receivers",
"-opt-in=kotlin.RequiresOptIn",
) )
// Treat all Kotlin warnings as errors (disabled by default) // Treat all Kotlin warnings as errors (disabled by default)

View File

@ -1 +0,0 @@
/build

View File

@ -1 +0,0 @@
/build

View File

@ -1 +0,0 @@
/build

View File

@ -6,10 +6,11 @@ plugins {
android { android {
namespace = "eu.kanade.tachiyomi.core.common" namespace = "eu.kanade.tachiyomi.core.common"
}
kotlinOptions { kotlin {
freeCompilerArgs += listOf( compilerOptions {
"-Xcontext-receivers", freeCompilerArgs.addAll(
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
) )

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.core.security
import tachiyomi.core.common.preference.PreferenceStore
class PrivacyPreferences(
private val preferenceStore: PreferenceStore,
) {
fun crashlytics() = preferenceStore.getBoolean("crashlytics", true)
fun analytics() = preferenceStore.getBoolean("analytics", true)
}

View File

@ -19,7 +19,7 @@ class NetworkPreferences(
fun defaultUserAgent(): Preference<String> { fun defaultUserAgent(): Preference<String> {
return preferenceStore.getString( return preferenceStore.getString(
"default_user_agent", "default_user_agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
) )
} }
} }

View File

@ -1,4 +1,4 @@
@file:Suppress("FunctionName", "ktlint:standard:function-naming") @file:Suppress("FunctionName")
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network

View File

@ -64,6 +64,7 @@ object DeviceUtil {
val invalidDefaultBrowsers = listOf( val invalidDefaultBrowsers = listOf(
"android", "android",
"com.hihonor.android.internal.app",
"com.huawei.android.internal.app", "com.huawei.android.internal.app",
"com.zui.resolver", "com.zui.resolver",
) )

View File

@ -6,7 +6,7 @@ import javax.microedition.khronos.egl.EGLContext
import kotlin.math.max import kotlin.math.max
object GLUtil { object GLUtil {
val maxTextureSize: Int by lazy { val DEVICE_TEXTURE_LIMIT: Int by lazy {
// Get EGL Display // Get EGL Display
val egl = EGLContext.getEGL() as EGL10 val egl = EGLContext.getEGL() as EGL10
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
@ -38,10 +38,23 @@ object GLUtil {
// Release // Release
egl.eglTerminate(display) egl.eglTerminate(display)
// Return largest texture size found, or default // Return largest texture size found (after making it a multiplier of [Multiplier]), or default
max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION) max(maximumTextureSize, SAFE_TEXTURE_LIMIT)
}
const val SAFE_TEXTURE_LIMIT: Int = 2048
val CUSTOM_TEXTURE_LIMIT_OPTIONS: List<Int> by lazy {
val steps = DEVICE_TEXTURE_LIMIT / MULTIPLIER
buildList(steps) {
add(DEVICE_TEXTURE_LIMIT)
for (step in steps downTo 2) {
val value = step * MULTIPLIER
if (value >= DEVICE_TEXTURE_LIMIT) continue
add(value)
}
}
} }
} }
// Safe minimum default size private const val MULTIPLIER: Int = 1024
private const val IMAGE_MAX_BITMAP_DIMENSION = 2048

View File

@ -36,7 +36,7 @@ object WebViewUtil {
fun getVersion(context: Context): String { fun getVersion(context: Context): String {
val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?" val webView = WebView.getCurrentWebViewPackage() ?: return "how did you get here?"
val pm = context.packageManager val pm = context.packageManager
val label = webView.applicationInfo.loadLabel(pm) val label = webView.applicationInfo!!.loadLabel(pm)
val version = webView.versionName val version = webView.versionName
return "$label $version" return "$label $version"
} }
@ -68,7 +68,6 @@ fun WebView.setDefaultSettings() {
with(settings) { with(settings) {
javaScriptEnabled = true javaScriptEnabled = true
domStorageEnabled = true domStorageEnabled = true
databaseEnabled = true
useWideViewPort = true useWideViewPort = true
loadWithOverviewMode = true loadWithOverviewMode = true
cacheMode = WebSettings.LOAD_DEFAULT cacheMode = WebSettings.LOAD_DEFAULT

View File

@ -22,6 +22,7 @@ import androidx.core.graphics.get
import androidx.core.graphics.green import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.system.GLUtil
import logcat.LogPriority import logcat.LogPriority
import okio.Buffer import okio.Buffer
import okio.BufferedSource import okio.BufferedSource
@ -309,6 +310,23 @@ object ImageUtil {
val bottomOffset = topOffset + splitHeight val bottomOffset = topOffset + splitHeight
} }
fun canUseHardwareBitmap(bitmap: Bitmap): Boolean {
return canUseHardwareBitmap(bitmap.width, bitmap.height)
}
fun canUseHardwareBitmap(imageSource: BufferedSource): Boolean {
return with(extractImageOptions(imageSource)) {
canUseHardwareBitmap(outWidth, outHeight)
}
}
var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT
private fun canUseHardwareBitmap(width: Int, height: Int): Boolean {
if (HARDWARE_BITMAP_UNSUPPORTED) return false
return maxOf(width, height) <= hardwareBitmapThreshold
}
/** /**
* Algorithm for determining what background to accompany a comic/manga page * Algorithm for determining what background to accompany a comic/manga page
*/ */
@ -555,6 +573,121 @@ object ImageUtil {
} }
private val optimalImageHeight = getDisplayMaxHeightInPx * 2 private val optimalImageHeight = getDisplayMaxHeightInPx * 2
/**
* Taken from Coil
* (https://github.com/coil-kt/coil/blob/1674d3516f061aeacbe749a435b1924f9648fd41/coil-core/src/androidMain/kotlin/coil3/util/hardwareBitmaps.kt)
* ---
* Maintains a list of devices with broken/incomplete/unstable hardware bitmap implementations.
*
* Model names are retrieved from
* [Google's official device list](https://support.google.com/googleplay/answer/1727131?hl=en).
*
*/
val HARDWARE_BITMAP_UNSUPPORTED = when (Build.VERSION.SDK_INT) {
26 -> run {
val model = Build.MODEL ?: return@run false
// Samsung Galaxy (ALL)
if (model.removePrefix("SAMSUNG-").startsWith("SM-")) return@run true
val device = Build.DEVICE ?: return@run false
return@run device in arrayOf(
"nora", "nora_8917", "nora_8917_n", // Moto E5
"james", "rjames_f", "rjames_go", "pettyl", // Moto E5 Play
"hannah", "ahannah", "rhannah", // Moto E5 Plus
"ali", "ali_n", // Moto G6
"aljeter", "aljeter_n", "jeter", // Moto G6 Play
"evert", "evert_n", "evert_nt", // Moto G6 Plus
"G3112", "G3116", "G3121", "G3123", "G3125", // Xperia XA1
"G3412", "G3416", "G3421", "G3423", "G3426", // Xperia XA1 Plus
"G3212", "G3221", "G3223", "G3226", // Xperia XA1 Ultra
"BV6800Pro", // BlackView BV6800Pro
"CatS41", // Cat S41
"Hi9Pro", // CHUWI Hi9 Pro
"manning", // Lenovo K8 Note
"N5702L", // NUU Mobile G3
)
}
27 -> run {
val device = Build.DEVICE ?: return@run false
return@run device in arrayOf(
"mcv1s", // LG Tribute Empire
"mcv3", // LG K11
"mcv5a", // LG Q7
"mcv7a", // LG Stylo 4
"A30ATMO", // T-Mobile REVVL 2
"A70AXLTMO", // T-Mobile REVVL 2 PLUS
"A3A_8_4G_TMO", // Alcatel 9027W
"Edison_CKT", // Alcatel ONYX
"EDISON_TF", // Alcatel TCL XL2
"FERMI_TF", // Alcatel A501DL
"U50A_ATT", // Alcatel TETRA
"U50A_PLUS_ATT", // Alcatel 5059R
"U50A_PLUS_TF", // Alcatel TCL LX
"U50APLUSTMO", // Alcatel 5059Z
"U5A_PLUS_4G", // Alcatel 1X
"RCT6513W87DK5e", // RCA Galileo Pro
"RCT6873W42BMF9A", // RCA Voyager
"RCT6A03W13", // RCA 10 Viking
"RCT6B03W12", // RCA Atlas 10 Pro
"RCT6B03W13", // RCA Atlas 10 Pro+
"RCT6T06E13", // RCA Artemis 10
"A3_Pro", // Umidigi A3 Pro
"One", // Umidigi One
"One_Max", // Umidigi One Max
"One_Pro", // Umidigi One Pro
"Z2", // Umidigi Z2
"Z2_PRO", // Umidigi Z2 Pro
"Armor_3", // Ulefone Armor 3
"Armor_6", // Ulefone Armor 6
"Blackview", // Blackview BV6000
"BV9500", // Blackview BV9500
"BV9500Pro", // Blackview BV9500Pro
"A6L-C", // Nuu A6L-C
"N5002LA", // Nuu A7L
"N5501LA", // Nuu A5L
"Power_2_Pro", // Leagoo Power 2 Pro
"Power_5", // Leagoo Power 5
"Z9", // Leagoo Z9
"V0310WW", // Blu VIVO VI+
"V0330WW", // Blu VIVO XI
"A3", // BenQ A3
"ASUS_X018_4", // Asus ZenFone Max Plus M1 (ZB570TL)
"C210AE", // Wiko Life
"fireball", // DROID Incredible 4G LTE
"ILA_X1", // iLA X1
"Infinix-X605_sprout", // Infinix NOTE 5 Stylus
"j7maxlte", // Samsung Galaxy J7 Max
"KING_KONG_3", // Cubot King Kong 3
"M10500", // Packard Bell M10500
"S70", // Altice ALTICE S70
"S80Lite", // Doogee S80Lite
"SGINO6", // SGiNO 6
"st18c10bnn", // Barnes and Noble BNTV650
"TECNO-CA8", // Tecno CAMON X Pro,
"SHIFT6m", // SHIFT 6m
)
}
else -> false
}
} }
val getDisplayMaxHeightInPx: Int val getDisplayMaxHeightInPx: Int

1
data/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -23,6 +23,12 @@ android {
} }
} }
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi")
}
}
dependencies { dependencies {
implementation(projects.sourceApi) implementation(projects.sourceApi)
implementation(projects.domain) implementation(projects.domain)
@ -30,12 +36,3 @@ dependencies {
api(libs.bundles.sqldelight) api(libs.bundles.sqldelight)
} }
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
}

View File

@ -49,6 +49,10 @@ class MangaRepositoryImpl(
return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) } return handler.awaitList { mangasQueries.getFavorites(MangaMapper::mapManga) }
} }
override suspend fun getReadMangaNotInLibrary(): List<Manga> {
return handler.awaitList { mangasQueries.getReadMangaNotInLibrary(MangaMapper::mapManga) }
}
override suspend fun getLibraryManga(): List<LibraryManga> { override suspend fun getLibraryManga(): List<LibraryManga> {
return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) } return handler.awaitList { libraryViewQueries.library(MangaMapper::mapLibraryManga) }
} }

View File

@ -78,6 +78,15 @@ SELECT *
FROM mangas FROM mangas
WHERE favorite = 1; WHERE favorite = 1;
getReadMangaNotInLibrary:
SELECT *
FROM mangas
WHERE favorite = 0 AND _id IN (
SELECT DISTINCT chapters.manga_id
FROM chapters
WHERE read = 1 OR last_page_read != 0
);
getAllManga: getAllManga:
SELECT * SELECT *
FROM mangas; FROM mangas;

1
domain/.gitignore vendored
View File

@ -1 +0,0 @@
/build

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