Compare commits

...

168 Commits

Author SHA1 Message Date
scb261
0d19308054
Rework website update workflow (#1851) 2025-03-11 14:39:02 +06:00
AntsyLich
aa6a4953c5
Fix firebase telemetry implementation package 2025-03-11 13:03:14 +06:00
AntsyLich
046f09c4bd
Move firebase stuff to a standalone module (#1841) 2025-03-11 06:55:50 +00:00
MajorTanya
eddf07f9ac
Add app ID to debug info (#1847)
This will avoid the need to know which forks has which version numbers
and avoid confusion in support.
2025-03-10 02:43:25 +00:00
AntsyLich
22b5fb58ff
Make option to mark duplicate chapter as read apply when reading (#1839) 2025-03-09 06:37:14 +00:00
AntsyLich
4f06c1cc09
Make more sliders discrete and ensure they don't look out of place (#1840)
Also cleanup the underlying code
2025-03-09 12:28:24 +06:00
AntsyLich
7913679f9d
Fix analytics/crashlytics configuration not being set after fe22f5a (#1838) 2025-03-08 19:46:12 +00:00
AntsyLich
0893609ad2
Change foss variant application id suffix to '.foss' and more (#1831)
- Remove `BuildConfig.PREVIEW`
- Rename `BuildConfig.INCLUDE_ANALYTICS` -> `BuildConfig.ANALYTICS_INCLUDED`
- Rename `BuildConfig.INCLUDE_UPDATER` -> `BuildConfig.UPDATER_ENABLED`
- Rename build property `with-analytics` -> `include-analytics`
- Rename build property `with-updater` -> `enable-updater`
- Add build property to disable code shrink
- Add build property to include dependency info in apk/app bundle
2025-03-08 03:10:56 +06:00
AntsyLich
85d168ed5e
Change label of setting to always use SSIV in long strip reader (#1834) 2025-03-07 13:03:52 +00:00
AntsyLich
d3691cc256
Bump default user agent (#1833) 2025-03-07 10:08:55 +06:00
AntsyLich
7f9406aec9
Fix website repo API url in workflow [skip ci] 2025-03-07 00:29:26 +06:00
FlaminSarge
563bc02113
Attempt to fix crash when migrating or removing entries from library (#1828)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-03-06 09:51:09 +00:00
NarwhalHorns
b702603965
Display staff information on Anilist tracker search results (#1810)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-03-06 08:21:05 +00:00
Roshan Varughese
2e2f1ed82d
Fix an issue where tracker reading progress is changed to a lower value (#1795) 2025-03-06 14:00:01 +06:00
Mend Renovate
b2765a00d2
Update dependency com.android.tools.build:gradle to v8.9.0 (#1824) 2025-03-05 19:03:54 +06:00
NarwhalHorns
0e6d6c087e
Update track search preview (#1825) 2025-03-05 08:20:09 +00:00
Smol Ame
4f7122d6f0
Tweak and adjust issue template (#1817)
Co-authored-by: BrutuZ <brutuz@users.noreply.github.com>
2025-03-05 12:44:00 +06:00
Mend Renovate
b763d3e2c2
Update dependency io.mockk:mockk to v1.13.17 (#1786) 2025-03-04 17:31:14 +06:00
AwkwardPeak7
9957fff2fb
Fix webview crash caused by 793d7fb (#1819) 2025-03-04 13:06:21 +06:00
AntsyLich
debca74e0d
Only update website on new release [skip ci] 2025-03-03 20:52:33 +06:00
scb261
1313ff7a16
Update website when a new version is released (#1818) 2025-03-03 20:49:39 +06:00
AwkwardPeak7
793d7fbe40
Spoof or remove X-Requested-With header from webview (#1812) 2025-03-02 21:16:42 +06:00
Mend Renovate
b12ee027ea
Update dependency com.google.firebase:firebase-bom to v33.10.0 (#1789) 2025-03-02 15:05:14 +06:00
Smol Ame
d7a1ae2734
Update Issue Request Template (#1808) 2025-03-02 14:47:41 +06:00
rhjdvsgsgks
7566918ee7
Add build tool version to android config (#1803) 2025-03-01 21:31:02 +00:00
AntsyLich
7b70b40d30
Set explicit runner version and specify build tools version for signing (#1787) 2025-02-27 19:22:22 +00:00
AntsyLich
cd0481592c
Add option to mark new duplicate read chapters as read (#1785) 2025-02-27 17:32:51 +00:00
Mend Renovate
b93746b01e
Update dependency com.android.tools.build:gradle to v8.8.2 (#1784) 2025-02-27 07:21:40 +00:00
AntsyLich
2b0c28938b
Remove alphabetical category sort option (#1781) 2025-02-27 13:11:41 +06:00
Mend Renovate
4db3817782
Update dependency androidx.activity:activity-compose to v1.10.1 (#1782) 2025-02-27 01:40:30 +06:00
Mend Renovate
ec07843f0c
Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 (#1783) 2025-02-27 01:40:18 +06:00
AntsyLich
b08b22fdcc
Fix APK paths in workflows 2025-02-27 01:21:32 +06:00
Cuong-Tran
919607cd06
Add back support for drag-and-drop category reordering (#1427) 2025-02-26 05:37:10 +00:00
Cuong-Tran
d91c7b6093
Add Xiaomi system app to list of invalid browsers (#1776) 2025-02-26 11:30:14 +06:00
Roshan Varughese
fab8b17d99
Add option to export minimal library information to a CSV file (#1161) 2025-02-26 05:09:50 +00:00
Roshan Varughese
8b28a9bcee
Stop showing divider when analytics is not included (#1778) 2025-02-26 04:30:02 +00:00
AntsyLich
79e25451bd
spotlessApply imports of 0dda64b9d80a47a96fb52d13b5e0ece6d5fca2b1 2025-02-26 02:24:53 +06:00
AntsyLich
0dda64b9d8
Use .toUri() extension function 2025-02-26 02:18:45 +06:00
AntsyLich
181dbbb638
Remove F-droid warnings 2025-02-26 02:13:14 +06:00
AntsyLich
3ce013fa19
Move google-services.json file to app module root 2025-02-26 01:59:02 +06:00
AntsyLich
fe22f5aa37
Rework build variants and add FOSS variant (#1775) 2025-02-26 00:17:36 +06:00
MajorTanya
1dd81ef1e1
Add private tracking support for Kitsu (#1774) 2025-02-25 13:26:32 +00:00
Mend Renovate
2d0be5b0c9
Update dependency gradle to v8.13 (#1773)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-25 11:38:12 +00:00
Mend Renovate
4d7350e318
Update dependency com.android.tools:desugar_jdk_libs to v2.1.5 (#1772) 2025-02-25 11:10:19 +00:00
NarwhalHorns
49b2b346b6
Support for private tracking with AniList and Bangumi (#1736)
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-25 11:01:13 +00:00
MajorTanya
badc229a23
Fix Bangumi login regression (#1770)
Caused by #1748.

Two different issues actually.

Firstly, the getUsername API call uses the authClient, which uses the
BangumiInterceptor to get the current OAuth data and attach the
Authorization header. However, on login, #1748 did not try to set the
new auth details until after attempting to call getUsername.
This would cause Mihon to think the user was not authenticated with
Bangumi and cancel the process.

This is fixed by having Mihon store the OAuth credentials in the
interceptor first before attempting to call getUsername.

The second issue is a simple trailing dollar sign in the API URL for
the getUsername method. This was removed.
2025-02-25 11:38:33 +06:00
MajorTanya
277d8bad8e
Add back explicit update(track) call to Bangumi (#1771)
Most if not all other trackers do this too. Technically this causes
some request duplication (since things like the BaseTracker's
setRemoteLastChapterRead fire anyway due to the tracker sheet being
open. But considering the reduced number of requests in other places,
I think this is still acceptable.

This change will allow #1736 to proceed, hopefully.
2025-02-25 11:37:23 +06:00
MajorTanya
8b48d1016b
Add "Monochrome" theme (#1752)
This theme is mainly geared towards e-Ink displays with limited/no
colour capabilities. Previous themes like Yin & Yang would make heavy
use of greyscale colours which could look off on some devices.

This theme is probably not conformant to Material Design 3 colour
scheme guidelines, but it does boast some amazing WebAIM contrast
ratios (#FFFFFF text on #000000 background gets a ratio of 21:1, vice
versa too).

Initially, this was intended as a purely black and white theme but
some contrast issues arose, such as the download badges (tertiary
background, onTertiary text colour) having the same colour as unread
badges (primary/onPrimary), or the step indicators (stops) not being
visible on sliders (since they use the colours of the opposite state
track (active region stops are the colour of the inactive region track
and vice versa).

To mitigate this, each variant (dark/light) of the theme has one
additional grey mixed in for their tertiary and secondaryContainer
colours each. For the dark variant, this is a #A0A0A0 background for
#000000 text (8.03:1 contrast ratio) and for the light variant, it is
a #505050 background for #FFFFFF text (8.06:1 contrast ratio).
This results in distinct unread vs download badges and visible steps
in the sliders.

---------

Co-authored-by: Sunspark-007 <73711243+Sunspark-007@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-25 05:22:48 +06:00
MajorTanya
a96fbba3dc
Migrate to Bangumi's newer v0 API (#1748)
This comes with many benefits:
- Starting dates are now available and shown to users
- Lays groundwork to add private tracking for Bangumi, e.g. in #1736
- Mihon makes approximately 2-4 times fewer calls to Bangumi's API
- Simplified interceptor for the access token addition
  - v0 does not allow access tokens in the query string
- There is actively maintained documentation for it

Also shrunk the DTOs for Bangumi by removing attributes we have no
use for either now or in the foreseeable future. Volume data remains
in case Mihon wants to ever support volumes. But attributes such as
user avatars, nicknames, data relating to Bangumi's tag & meta-tag
systems, etc. have been removed or just not added to the DTOs.
2025-02-24 23:21:22 +00:00
Mend Renovate
d8a530266f
Update dependency androidx.compose:compose-bom to v2025 (#1651) 2025-02-25 05:20:31 +06:00
Cuong-Tran
e1724d1aa0
Fix backup/restore of category related preferences (#1726)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-24 22:04:39 +00:00
AntsyLich
1a5b4c2804
Format CHANGELOG.md 2025-02-25 03:43:52 +06:00
Cuong-Tran
2cd52d5a1f
Reuse AppBar in manga screen (#1367)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-25 03:33:47 +06:00
AntsyLich
ebfbbf0741
Cleanup and tweak preference widgets (#1769) 2025-02-24 20:19:25 +00:00
Cuong-Tran
eeb683069a
Fix App's preferences referencing deleted categories (#1734) 2025-02-25 00:44:28 +06:00
Roshan Varughese
7e71a34256
Add button to favorite manga from history screen (#1733) 2025-02-24 22:20:20 +06:00
NGB-Was-Taken
29ee53f461
Apply "Downloaded only" filter to all entries regardless of favourite status (#1603)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-24 22:08:09 +06:00
Mend Renovate
6a223f34a0
Update aboutlib.version to v11.6.3 (#1737) 2025-02-23 22:47:31 +06:00
Mend Renovate
c335ea9103
Update actions/upload-artifact action to v4.6.1 (#1756) 2025-02-23 22:45:59 +06:00
BrutuZ
c97fe71e29
Ignore hidden files/folders for Local Source chapter list (#1763) 2025-02-23 10:55:03 +06:00
Mend Renovate
8e81a5e68b
Update aboutlib.version to v11.6.0 (#1728) 2025-02-16 01:52:34 +06:00
Mend Renovate
b08270d523
Update plugin firebase-crashlytics to v3.0.3 (#1702) 2025-02-15 20:26:33 +06:00
AntsyLich
34d1e6fa27
Add more editor configs and move ktlint config to it (#1731) 2025-02-15 13:51:10 +00:00
Mend Renovate
a80965f7f1
Update dependency com.android.tools.build:gradle to v8.8.1 (#1723) 2025-02-14 02:44:55 +06:00
Mend Renovate
59ee61039b
Update paging.version to v3.3.6 (#1717) 2025-02-14 02:41:42 +06:00
Mend Renovate
b7a96e6946
Update dependency io.coil-kt.coil3:coil-bom to v3.1.0 (#1701) 2025-02-14 02:37:21 +06:00
Mend Renovate
31a3f9e051
Update moko to v0.24.5 (#1694) 2025-02-14 02:37:00 +06:00
Mend Renovate
42e45e6020
Update GitHub Actions (#1677) 2025-02-14 02:35:21 +06:00
AntsyLich
e8c9cb2c2e
Rework slider UI
Fixes #1474
2025-02-05 23:37:30 +06:00
Mend Renovate
d592ab2e87
Update aboutlib.version to v11.5.0 (#1663) 2025-02-02 21:56:00 +06:00
Mend Renovate
9d6ed93daa
Update dependency gradle to v8.12.1 (#1662) 2025-02-02 21:55:41 +06:00
Mend Renovate
34efa8d901
Update kotlin monorepo to v2.1.10 (#1671) 2025-02-02 21:55:27 +06:00
MajorTanya
bfc8320aa4
Add Infinix system app to list of invalid browsers (#1684)
* Add Infinix system app to list of invalid browsers

`com.transsion.resolver` being picked by the system as a suitable
browser caused a Mihon user with an Infinix device to be unable to
open any links in browsers, including tracker login and opening a
WebView page in a real browser.

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* Add docstring to DeviceUtil.invalidDefaultBrowsers

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-02-02 21:54:55 +06:00
MajorTanya
29ec7c125a
Fix MAL tracker losing track of login expiration (#1682)
* Add missing @EncodeDefault annotation to MALOAuth

Similar to the situation with Bangumi, the missing annotation means
kotlinx.serialization would _provide_ the default value upon
instantiation but not serialise it to disk. This means the isExpired()
calculation would effectively rarely/never do its job correctly,
leading to Mihon sending expired tokens to MAL and causing problems
for everyone involved.

Overall, this change _could_ (should) lead to a drastic reduction in
MAL requests failing, leading to users having to relink their MAL
accounts.

Also switched createdAt to be in seconds instead of milliseconds as
all other trackers use seconds for timestamps (except for AniList,
which uses milliseconds but doesn't use a createdAt timestamp anyway).

* Add CHANGELOG.md entry
2025-01-30 15:43:37 +00:00
MajorTanya
dce6aacf02
Fix Bangumi tracker losing track of login expiration (#1681)
* Fix Bangumi tracking losing track of login state

kotlinx.serialization does NOT serialize default values (like
createdAt in BGMOAuth.kt), so every time the Bangumi tracker
deserialized the tracker OAuth, createdAt was set to the time of the
read, not the time of issuance.

Separately, BangumiInterceptor did correctly fetch new OAuth
credentials upon detected expiry of the stored credentials and saved
them, but did not use them for the current request (the new
credentials were used for all subsequent requests only). This led to
401 errors from Bangumi because the expired access_token was provided.
 A subsequent request using the newly acquired access_token would end
 up being successful.

* Add CHANGELOG.md entry
2025-01-30 21:29:03 +06:00
MajorTanya
503d0be667
Add zoned "Current time" to debug info and include year & timezone in logcat output (#1672)
* Add zoned date & time to debug info & logs

This should help distinguish log entries that happened recently and
may be related to crashes from older entries that occurred before now.

* Change logcat date and time output format

After some discussion, it was decided to adjust the logcat date and
time display to include the year and the timezone in the logcat
output. This results in a line start like this:

`2025-01-27 18:37:46.662 +0100`

which follows the following DateTimeFormatter pattern:

`yyyy-MM-dd HH:mm:ss.SSS Z`

* Add CHANGELOG.md entry
2025-01-28 15:53:26 +06:00
Mend Renovate
82fd89cee6
Update dependency com.google.firebase:firebase-bom to v33.8.0 (#1652) 2025-01-21 21:39:44 +06:00
Mend Renovate
643f95f046
Update dependency androidx.recyclerview:recyclerview to v1.4.0 (#1650) 2025-01-21 21:33:14 +06:00
Mend Renovate
9c81f2486c
Update dependency androidx.activity:activity-compose to v1.10.0 (#1649) 2025-01-21 21:32:08 +06:00
Mend Renovate
e59d2d381d
Update dependency com.diffplug.spotless:spotless-plugin-gradle to v7.0.2 (#1647) 2025-01-21 21:30:34 +06:00
AntsyLich
da90064c94
Remove unnecessary filters for pseudolocales 2025-01-21 09:01:32 +06:00
AntsyLich
d53a3828b1
Address some deprecations 2025-01-21 08:55:34 +06:00
sdaqo
c283abefb0
Add option to enable incognito mode per extension (#157)
* add per Extension Incognito Mode

* migrate incognito sources when extension is updated

* remove incognito sources when extension is uninstalled

* remove not used variable

* address change requests

address change requests

* Rebase and cleanup code

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-01-12 09:38:05 +06:00
Mend Renovate
4bc593861c
Update GitHub Actions (#1632) 2025-01-12 05:01:17 +06:00
Mend Renovate
5a9367603b
Update dependency com.diffplug.spotless:spotless-plugin-gradle to v7.0.1 (#1630) 2025-01-12 05:00:59 +06:00
Mend Renovate
c01e9f3e92
Update dependency com.squareup.okio:okio to v3.10.2 (#1631) 2025-01-12 05:00:42 +06:00
Mend Renovate
ae9753a1ea
Update dependency com.android.tools.build:gradle to v8.8.0 (#1634) 2025-01-12 05:00:27 +06:00
Mend Renovate
1fe4d6cbd4
Update dependency io.mockk:mockk to v1.13.16 (#1636) 2025-01-12 05:00:11 +06:00
AntsyLich
2c5f28f277
[skip ci] Use Adoptium distributed Java in workflows 2025-01-10 05:00:42 +06:00
Mend Renovate
3a3abc6854
Update dependency com.diffplug.spotless:spotless-plugin-gradle to v7.0.0 (#1628) 2025-01-07 16:35:21 +06:00
Mend Renovate
d9a550b935
Update serialization.version to v1.8.0 (#1627) 2025-01-07 16:35:09 +06:00
Mend Renovate
1617f8eb49
Update aboutlib.version to v11.4.0 (#1621) 2025-01-07 16:34:39 +06:00
AntsyLich
77faab14b1
Tweak build workflows 2025-01-07 14:33:08 +06:00
MajorTanya
d60802721b
Fix MAL main_picture nullability breaking search if a result doesn't have a cover set (#1618)
* Fix MAL manga cover nullability

If a manga doesn't have a cover, MAL doesn't provide the
`main_picture` element in the API response at all.

* Add CHANGELOG.md entry
2025-01-04 04:19:11 +06:00
Mend Renovate
19af85ab61
Update GitHub Actions (#1561)
* Update GitHub Actions

* Downgrade softprops/action-gh-release

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-01-02 15:25:04 +06: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 1909126921ac78309f7f7c7c2aa85606611531b8
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 bb4d9fc81a043ac4f2d0105f19c09974ae2f7201.
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
265 changed files with 5253 additions and 1972 deletions

View File

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

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: ❌ Help with Extensions
url: https://mihon.app/docs/faq/browse/extensions
about: For extension-related questions/issues
- name: 🖥️ Mihon website
url: https://mihon.app/
about: Guides, troubleshooting, and answers to common questions

View File

@ -43,9 +43,9 @@ body:
attributes:
label: Crash logs
description: |
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
If you're experiencing crashes, if possible, go to the app's **More → Settings → Advanced** page, press **Dump crash logs** and share the crash logs here.
placeholder: |
You can paste the crash logs in plain text or upload it as an attachment.
You can upload the crash log file as an attachment, or paste the crash logs in plain text if needed.
- type: input
id: mihon-version
@ -53,7 +53,7 @@ body:
label: Mihon version
description: You can find your Mihon version in **More → About**.
placeholder: |
Example: "0.17.0"
Example: "0.17.1"
validations:
required: true
@ -96,9 +96,9 @@ body:
required: true
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.17.0](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
- label: I have updated all installed extensions.
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
- label: I will fill out all of the requested information in this form.
- label: I understand that **Mihon does not have or fix any extensions**, and I **will not receive help** for any issues related to sources or extensions.
required: true

View File

@ -31,7 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.17.0](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
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"extends": ["config:recommended"],
"labels": ["Dependencies"],
"semanticCommits": "disabled",
"packageRules": [
@ -8,6 +8,6 @@
"groupName": "GitHub Actions",
"matchManagers": ["github-actions"],
"pinDigests": true,
}
]
},
],
}

View File

@ -19,38 +19,41 @@ permissions:
jobs:
build:
name: Build app
runs-on: ubuntu-latest
runs-on: 'ubuntu-24.04'
steps:
- name: Clone repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Dependency Review
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
- name: Set up JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: 17
distribution: adopt
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: Build app and run unit tests
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Check code format
run: ./gradlew spotlessCheck
- name: Build app
run: ./gradlew assembleRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest
- name: Upload APK
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
- name: Upload mapping
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/standardRelease
path: app/build/outputs/mapping/release

View File

@ -13,42 +13,41 @@ concurrency:
jobs:
build:
name: Build app
runs-on: ubuntu-latest
runs-on: 'ubuntu-24.04'
steps:
- name: Clone repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Setup Android SDK
run: |
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3"
- name: Set up JDK
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: 17
distribution: adopt
distribution: temurin
- name: Set up gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: Build app and run unit tests
run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest
- name: Check code format
run: ./gradlew spotlessCheck
- name: Build app
run: ./gradlew assembleRelease -Pinclude-analytics -Penable-updater
- name: Run unit tests
run: ./gradlew testReleaseUnitTest
- name: Upload APK
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
- name: Upload mapping
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/standardRelease
path: app/build/outputs/mapping/release
# Sign APK and create release for tags
@ -60,9 +59,9 @@ jobs:
- name: Sign APK
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
with:
releaseDirectory: app/build/outputs/apk/standard/release
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@ -73,29 +72,29 @@ jobs:
run: |
set -e
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk
sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV
- name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon'
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
tag_name: ${{ env.VERSION_TAG }}
name: Mihon ${{ env.VERSION_TAG }}
@ -111,7 +110,7 @@ jobs:
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
| x86 | ${{ env.APK_X86_SHA }} |
| x86_64 | ${{ env.APK_X86_64_SHA }} |
## If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk
files: |
mihon-${{ env.VERSION_TAG }}.apk
@ -121,5 +120,4 @@ jobs:
mihon-x86_64-${{ env.VERSION_TAG }}.apk
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
token: ${{ secrets.MIHON_BOT_TOKEN }}

23
.github/workflows/update_website.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Update website
on:
release:
types:
- published
- deleted
- edited
jobs:
update_website:
runs-on: 'ubuntu-24.04'
steps:
- name: Update website
run: |
curl --fail-with-body -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.MIHON_BOT_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/mihonapp/website/dispatches \
-d '{"event_type":"app_release"}'

2
.gitignore vendored
View File

@ -6,7 +6,7 @@ build
# IDE files
*.iml
.idea/*
!.idea/icon.png
!.idea/icon.svg
/captures
# Configuration files

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

@ -11,6 +11,65 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [Unreleased]
### Added
- Add option to always decode long strip images with SSIV
- Change option label ([@AntsyLich](https://github.com/AntsyLich)) ([#1835](https://github.com/mihonapp/mihon/pull/1835))
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
- Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161))
- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427))
- Add option to mark duplicate read chapters as read
- Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810))
### Changed
- Sliders UI
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
- Ignore hidden files/folders for Local Source chapter list ([@BrutuZ](https://github.com/BrutuZ)) ([#1763](https://github.com/mihonapp/mihon/pull/1763))
- Migrate to newer Bangumi API ([@MajorTanya](https://github.com/MajorTanya)) ([#1748](https://github.com/mihonapp/mihon/pull/1748))
- Now showing manga starting dates in search
- Reduced request load by 2-4x in certain situations
- Bump default user agent
### Fixed
- Fix MAL `main_picture` nullability breaking search if a result doesn't have a cover set ([@MajorTanya](https://github.com/MajorTanya)) ([#1618](https://github.com/mihonapp/mihon/pull/1618))
- Fix Bangumi and MAL tracking 401 errors due to Mihon sending expired credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#1681](https://github.com/mihonapp/mihon/pull/1681), [#1682](https://github.com/mihonapp/mihon/pull/1682))
- Fix certain Infinix, Xiaomi devices being unable to use any "Open link in browser" actions, including tracker setup ([@MajorTanya](https://github.com/MajorTanya)) ([#1684](https://github.com/mihonapp/mihon/pull/1684)) ([#1776](https://github.com/mihonapp/mihon/pull/1776))
- Fix App's preferences referencing deleted categories ([@cuong-tran](https://github.com/cuong-tran)) ([#1734](https://github.com/mihonapp/mihon/pull/1734))
- Fix backup/restore of category related preferences ([@cuong-tran](https://github.com/cuong-tran)) ([#1726](https://github.com/mihonapp/mihon/pull/1726))
- Fix WebView sending app's package name in `X-Requested-With` header, which led to sources blocking access ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1812](https://github.com/mihonapp/mihon/pull/1812))
- Fix an issue where tracker reading progress is changed to a lower value ([@Animeboynz](https://github.com/Animeboynz)) ([#1795](https://github.com/mihonapp/mihon/pull/1795))
- Attempt to fix crash when migrating or removing entries from library ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1828](https://github.com/mihonapp/mihon/pull/1828))
### Removed
- Remove alphabetical category sort option
### Other
- Add zoned "Current time" to debug info and include year & timezone in logcat output ([@MajorTanya](https://github.com/MajorTanya)) ([#1672](https://github.com/mihonapp/mihon/pull/1672))
- Add application package ID to debug info ([@MajorTanya](https://github.com/MajorTanya)) ([#1847](https://github.com/mihonapp/mihon/pull/1847))
## [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
@ -39,7 +98,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- 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))
- 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))
@ -57,7 +116,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- 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))-
- 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))
@ -128,7 +187,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Other
- Code cleanup
- Minor refactor of theming when expressions ([@MajorTanya](https://github.com/MajorTanya)) ([#396](https://github.com/mihonapp/mihon/pull/396))
- 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))
@ -157,7 +216,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- 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))
- 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))
@ -258,7 +318,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- 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))
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.17.0...main
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.17.1...main
[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
[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

View File

@ -68,7 +68,7 @@ The developer(s) of this application does not have any affiliation with the cont
<pre>
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");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
import mihon.buildlogic.Config
import mihon.buildlogic.getBuildTime
import mihon.buildlogic.getCommitCount
import mihon.buildlogic.getGitSha
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("mihon.android.application")
@ -11,7 +11,7 @@ plugins {
alias(libs.plugins.aboutLibraries)
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
if (Config.includeAnalytics) {
pluginManager.apply {
apply(libs.plugins.google.services.get().pluginId)
apply(libs.plugins.firebase.crashlytics.get().pluginId)
@ -20,69 +20,67 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
shortcutHelper.setFilePath("./shortcuts.xml")
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
namespace = "eu.kanade.tachiyomi"
defaultConfig {
applicationId = "app.mihon"
versionCode = 8
versionName = "0.17.0"
versionCode = 10
versionName = "0.17.1"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
buildConfigField("boolean", "PREVIEW", "false")
ndk {
abiFilters += supportedAbis
}
buildConfigField("boolean", "ANALYTICS_INCLUDED", "${Config.includeAnalytics}")
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
splits {
abi {
isEnable = true
reset()
include(*supportedAbis.toTypedArray())
isUniversalApk = true
}
}
buildTypes {
named("debug") {
val debug by getting {
applicationIdSuffix = ".dev"
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true
}
named("release") {
isShrinkResources = true
isMinifyEnabled = true
val release by getting {
isMinifyEnabled = Config.enableCodeShrink
isShrinkResources = Config.enableCodeShrink
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
}
create("preview") {
initWith(getByName("release"))
buildConfigField("boolean", "PREVIEW", "true")
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
val debugType = getByName("debug")
versionNameSuffix = debugType.versionNameSuffix
applicationIdSuffix = debugType.applicationIdSuffix
val commonMatchingFallbacks = listOf(release.name)
create("foss") {
initWith(release)
applicationIdSuffix = ".foss"
matchingFallbacks.addAll(commonMatchingFallbacks)
}
create("preview") {
initWith(release)
applicationIdSuffix = ".debug"
versionNameSuffix = debug.versionNameSuffix
signingConfig = debug.signingConfig
matchingFallbacks.addAll(commonMatchingFallbacks)
}
create("benchmark") {
initWith(getByName("release"))
initWith(release)
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
signingConfig = debug.signingConfig
matchingFallbacks.addAll(commonMatchingFallbacks)
}
}
@ -91,39 +89,46 @@ android {
getByName("benchmark").res.srcDirs("src/debug/res")
}
flavorDimensions.add("default")
productFlavors {
create("standard") {
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
dimension = "default"
}
create("dev") {
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
dimension = "default"
splits {
abi {
isEnable = true
isUniversalApk = true
reset()
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
packaging {
resources.excludes.addAll(
listOf(
jniLibs {
keepDebugSymbols += listOf(
"libandroidx.graphics.path",
"libarchive-jni",
"libconscrypt_jni",
"libimagedecoder",
"libquickjs",
"libsqlite3x",
)
.map { "**/$it.so" }
}
resources {
excludes += setOf(
"kotlin-tooling-metadata.json",
"META-INF/DEPENDENCIES",
"LICENSE.txt",
"META-INF/LICENSE",
"META-INF/**/*.properties",
"META-INF/**/LICENSE.txt",
"META-INF/*.properties",
"META-INF/**/*.properties",
"META-INF/README.md",
"META-INF/NOTICE",
"META-INF/*.version",
),
)
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/NOTICE",
"META-INF/README.md",
)
}
}
dependenciesInfo {
includeInApk = false
includeInApk = Config.includeDependencyInfo
includeInBundle = Config.includeDependencyInfo
}
buildFeatures {
@ -142,6 +147,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 {
implementation(projects.i18n)
implementation(projects.core.archive)
@ -153,6 +176,7 @@ dependencies {
implementation(projects.domain)
implementation(projects.presentationCore)
implementation(projects.presentationWidget)
implementation(projects.telemetry)
// Compose
implementation(compose.activity)
@ -241,15 +265,11 @@ dependencies {
implementation(libs.swipe)
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
// Logging
implementation(libs.logcat)
// Crash reports/analytics
"standardImplementation"(platform(libs.firebase.bom))
"standardImplementation"(libs.firebase.analytics)
"standardImplementation"(libs.firebase.crashlytics)
// Shizuku
implementation(libs.bundles.shizuku)
@ -279,28 +299,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 {
dependencies {
classpath(kotlinx.gradle)

View File

@ -1,5 +1,6 @@
package eu.kanade.core.util
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -45,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].
*
@ -70,27 +56,7 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
@OptIn(ExperimentalContracts::class)
inline fun <T> List<T>.fastFilterNot(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 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
return fastFilter { !predicate(it) }
}
/**
@ -131,26 +97,3 @@ inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
fastForEach { if (predicate(it)) --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

@ -13,9 +13,11 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleIncognito
import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin
@ -109,7 +111,7 @@ class DomainModule : InjektModule {
addFactory { RenameCategory(get()) }
addFactory { ReorderCategory(get()) }
addFactory { UpdateCategory(get()) }
addFactory { DeleteCategory(get()) }
addFactory { DeleteCategory(get(), get(), get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetDuplicateLibraryManga(get()) }
@ -151,7 +153,7 @@ class DomainModule : InjektModule {
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { FilterChaptersForDownload(get(), get(), get()) }
@ -191,5 +193,7 @@ class DomainModule : InjektModule {
addFactory { DeleteExtensionRepo(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.base
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR
@ -30,4 +31,8 @@ class BasePreferences(
}
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

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import tachiyomi.data.chapter.ChapterSanitizer
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
@ -34,6 +35,7 @@ class SyncChaptersWithSource(
private val updateChapter: UpdateChapter,
private val getChaptersByMangaId: GetChaptersByMangaId,
private val getExcludedScanlators: GetExcludedScanlators,
private val readerPreferences: ReaderPreferences,
) {
/**
@ -145,12 +147,18 @@ class SyncChaptersWithSource(
return emptyList()
}
val reAdded = mutableListOf<Chapter>()
val changedOrDuplicateReadUrls = mutableSetOf<String>()
val deletedChapterNumbers = TreeSet<Double>()
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
val readChapterNumbers = dbChapters
.asSequence()
.filter { it.read && it.isRecognizedNumber }
.map { it.chapterNumber }
.toSet()
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
@ -160,12 +168,19 @@ class SyncChaptersWithSource(
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
val markDuplicateAsRead = readerPreferences.markDuplicateReadChapterAsRead().get()
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
changedOrDuplicateReadUrls.add(chapter.url)
chapter = chapter.copy(read = true)
}
if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
chapter = chapter.copy(
@ -178,7 +193,7 @@ class SyncChaptersWithSource(
chapter = chapter.copy(dateFetch = it)
}
reAdded.add(chapter)
changedOrDuplicateReadUrls.add(chapter.url)
chapter
}
@ -202,12 +217,8 @@ class SyncChaptersWithSource(
// Note that last_update actually represents last time the chapter list changed at all
updateManga.awaitUpdateLastUpdate(manga.id)
val reAddedUrls = reAdded.map { it.url }.toHashSet()
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
return updatedToAdd.filterNot {
it.url in reAddedUrls || it.scanlator in excludedScanlators
}
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
}
}

View File

@ -22,7 +22,7 @@ val Manga.readerOrientation: Long
val Manga.downloadedFilter: TriState
get() {
if (forceDownloaded()) return TriState.ENABLED_IS
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
@ -34,9 +34,6 @@ fun Manga.chaptersFiltered(): Boolean {
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Manga.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
}
fun Manga.toSManga(): SManga = SManga.create().also {
it.url = url

View File

@ -0,0 +1,35 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
class GetIncognitoState(
private val basePreferences: BasePreferences,
private val sourcePreferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {
fun await(sourceId: Long?): Boolean {
if (basePreferences.incognitoMode().get()) return true
if (sourceId == null) return false
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
return extensionPackage in sourcePreferences.incognitoExtensions().get()
}
fun subscribe(sourceId: Long?): Flow<Boolean> {
if (sourceId == null) return basePreferences.incognitoMode().changes()
return combine(
basePreferences.incognitoMode().changes(),
sourcePreferences.incognitoExtensions().changes(),
extensionManager.getExtensionPackageAsFlow(sourceId),
) { incognito, incognitoExtensions, extensionPackage ->
incognito || (extensionPackage in incognitoExtensions)
}
.distinctUntilChanged()
}
}

View File

@ -0,0 +1,14 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.common.preference.getAndSet
class ToggleIncognito(
private val preferences: SourcePreferences,
) {
fun await(extensions: String, enable: Boolean) {
preferences.incognitoExtensions().getAndSet {
if (enable) it.plus(extensions) else it.minus(extensions)
}
}
}

View File

@ -22,6 +22,8 @@ class SourcePreferences(
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
fun lastUsedSource() = preferenceStore.getLong(

View File

@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status,
startDate = other.startDate,
finishDate = other.finishDate,
private = other.private,
)
}
@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl
it.started_reading_date = startDate
it.finished_reading_date = finishDate
it.private = private
}
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url,
startDate = started_reading_date,
finishDate = finished_reading_date,
private = private,
)
}

View File

@ -1,8 +1,7 @@
package eu.kanade.domain.ui.model
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.i18n.MR
enum class AppTheme(val titleRes: StringResource?) {
@ -13,13 +12,14 @@ enum class AppTheme(val titleRes: StringResource?) {
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
// TODO: re-enable for preview
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
NORD(MR.strings.theme_nord.takeUnless { isReleaseBuildType }),
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
TAKO(MR.strings.theme_tako),
TEALTURQUOISE(MR.strings.theme_tealturquoise),
TIDAL_WAVE(MR.strings.theme_tidalwave),
YINYANG(MR.strings.theme_yinyang),
YOTSUBA(MR.strings.theme_yotsuba),
MONOCHROME(MR.strings.theme_monochrome),
// Deprecated
DARK_BLUE(null),

View File

@ -35,8 +35,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -48,6 +50,7 @@ import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
@ -72,6 +75,7 @@ fun ExtensionDetailsScreen(
onClickClearCookies: () -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
@ -140,9 +144,11 @@ fun ExtensionDetailsScreen(
contentPadding = paddingValues,
extension = state.extension,
sources = state.sources,
incognitoMode = state.isIncognito,
onClickSourcePreferences = onClickSourcePreferences,
onClickUninstall = onClickUninstall,
onClickSource = onClickSource,
onClickIncognito = onClickIncognito,
)
}
}
@ -152,9 +158,11 @@ private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
sources: ImmutableList<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickUninstall: () -> Unit,
onClickSource: (sourceId: Long) -> Unit,
onClickIncognito: (Boolean) -> Unit,
) {
val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) }
@ -171,6 +179,7 @@ private fun ExtensionDetails(
item {
DetailsHeader(
extension = extension,
extIncognitoMode = incognitoMode,
onClickUninstall = onClickUninstall,
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@ -182,6 +191,7 @@ private fun ExtensionDetails(
onClickAgeRating = {
showNsfwWarning = true
},
onExtIncognitoChange = onClickIncognito,
)
}
@ -209,9 +219,11 @@ private fun ExtensionDetails(
@Composable
private fun DetailsHeader(
extension: Extension,
extIncognitoMode: Boolean,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: (() -> Unit)?,
onExtIncognitoChange: (Boolean) -> Unit,
) {
val context = LocalContext.current
@ -219,9 +231,8 @@ private fun DetailsHeader(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium)
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
bottom = MaterialTheme.padding.small,
)
@ -313,12 +324,9 @@ private fun DetailsHeader(
}
Row(
modifier = Modifier.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium)
.padding(top = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
@ -341,6 +349,24 @@ private fun DetailsHeader(
}
}
TextPreferenceWidget(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
title = stringResource(MR.strings.pref_incognito_mode),
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
widget = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Switch(
checked = extIncognitoMode,
onCheckedChange = onExtIncognitoChange,
modifier = Modifier.padding(start = TrailingWidgetBuffer),
)
}
},
)
HorizontalDivider()
}
}

View File

@ -2,22 +2,24 @@ package eu.kanade.presentation.category
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.CategoryListItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
import kotlinx.collections.immutable.persistentListOf
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
@ -31,11 +33,9 @@ import tachiyomi.presentation.core.util.plus
fun CategoryScreen(
state: CategoryScreenState.Success,
onClickCreate: () -> Unit,
onClickSortAlphabetically: () -> Unit,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onClickMoveUp: (Category) -> Unit,
onClickMoveDown: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
navigateUp: () -> Unit,
) {
val lazyListState = rememberLazyListState()
@ -44,17 +44,6 @@ fun CategoryScreen(
AppBar(
title = stringResource(MR.strings.action_edit_categories),
navigateUp = navigateUp,
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
),
)
},
scrollBehavior = scrollBehavior,
)
},
@ -76,13 +65,10 @@ fun CategoryScreen(
CategoryContent(
categories = state.categories,
lazyListState = lazyListState,
paddingValues = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
paddingValues = paddingValues,
onClickRename = onClickRename,
onClickDelete = onClickDelete,
onMoveUp = onClickMoveUp,
onMoveDown = onClickMoveDown,
onChangeOrder = onChangeOrder,
)
}
}
@ -94,28 +80,44 @@ private fun CategoryContent(
paddingValues: PaddingValues,
onClickRename: (Category) -> Unit,
onClickDelete: (Category) -> Unit,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onChangeOrder: (Category, Int) -> Unit,
) {
val categoriesState = remember { categories.toMutableStateList() }
val reorderableState = rememberReorderableLazyListState(lazyListState, paddingValues) { from, to ->
val item = categoriesState.removeAt(from.index)
categoriesState.add(to.index, item)
onChangeOrder(item, to.index)
}
LaunchedEffect(categories) {
if (!reorderableState.isAnyItemDragging) {
categoriesState.clear()
categoriesState.addAll(categories)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = paddingValues,
contentPadding = paddingValues +
topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
itemsIndexed(
items = categories,
key = { _, category -> "category-${category.id}" },
) { index, category ->
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
canMoveUp = index != 0,
canMoveDown = index != categories.lastIndex,
onMoveUp = onMoveUp,
onMoveDown = onMoveDown,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
items(
items = categoriesState,
key = { category -> category.key },
) { category ->
ReorderableItem(reorderableState, category.key) {
CategoryListItem(
modifier = Modifier.animateItem(),
category = category,
onRename = { onClickRename(category) },
onDelete = { onClickDelete(category) },
)
}
}
}
}
private val Category.key inline get() = "category-$id"

View File

@ -193,35 +193,6 @@ fun CategoryDeleteDialog(
)
}
@Composable
fun CategorySortAlphabeticallyDialog(
onDismissRequest: () -> Unit,
onSort: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onSort()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_sort_category))
},
text = {
Text(text = stringResource(MR.strings.sort_category_confirmation))
},
)
}
@Composable
fun ChangeCategoryDialog(
initialSelection: ImmutableList<CheckboxState<Category>>,

View File

@ -2,14 +2,11 @@ package eu.kanade.presentation.category.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DragHandle
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -19,57 +16,42 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import sh.calvin.reorderable.ReorderableCollectionItemScope
import tachiyomi.domain.category.model.Category
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun CategoryListItem(
fun ReorderableCollectionItemScope.CategoryListItem(
category: Category,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: (Category) -> Unit,
onMoveDown: (Category) -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
) {
ElevatedCard(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onRename() }
.clickable(onClick = onRename)
.padding(vertical = MaterialTheme.padding.small)
.padding(
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Icon(
imageVector = Icons.Outlined.DragHandle,
contentDescription = null,
modifier = Modifier
.padding(MaterialTheme.padding.medium)
.draggableHandle(),
)
Text(
text = category.name,
modifier = Modifier
.padding(start = MaterialTheme.padding.medium),
modifier = Modifier.weight(1f),
)
}
Row {
IconButton(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {
Icon(
imageVector = Icons.Outlined.Edit,
@ -77,7 +59,10 @@ fun CategoryListItem(
)
}
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(MR.strings.action_delete),
)
}
}
}

View File

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

View File

@ -38,6 +38,7 @@ fun HistoryScreen(
onSearchQueryChange: (String?) -> Unit,
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onClickFavorite: (mangaId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
) {
Scaffold(
@ -84,6 +85,7 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
onClickFavorite = { history -> onClickFavorite(history.mangaId) },
)
}
}
@ -97,6 +99,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
onClickFavorite: (HistoryWithRelations) -> Unit,
) {
FastScrollLazyColumn(
contentPadding = contentPadding,
@ -126,6 +129,7 @@ private fun HistoryScreenContent(
onClickCover = { onClickCover(value) },
onClickResume = { onClickResume(value) },
onClickDelete = { onClickDelete(value) },
onClickFavorite = { onClickFavorite(value) },
)
}
}
@ -152,6 +156,7 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
onClickFavorite = {},
)
}
}

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -39,6 +40,7 @@ fun HistoryItem(
onClickCover: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
onClickFavorite: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@ -82,6 +84,16 @@ fun HistoryItem(
)
}
if (!history.coverData.isMangaFavorite) {
IconButton(onClick = onClickFavorite) {
Icon(
imageVector = Icons.Outlined.FavoriteBorder,
contentDescription = stringResource(MR.strings.add_to_library),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
@ -105,6 +117,7 @@ private fun HistoryItemPreviews(
onClickCover = {},
onClickResume = {},
onClickDelete = {},
onClickFavorite = {},
)
}
}

View File

@ -9,6 +9,7 @@ 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.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -19,8 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.preference.TriState
import tachiyomi.domain.category.model.Category
@ -117,10 +117,7 @@ private fun ColumnScope.FilterPage(
onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
)
// TODO: re-enable when custom intervals are ready for stable
if (
(isDevFlavor || isPreviewBuildType) &&
LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions
) {
if ((!isReleaseBuildType) && LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in autoUpdateMangaRestrictions) {
val filterIntervalCustom by screenModel.libraryPreferences.filterIntervalCustom().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_interval_custom),
@ -255,15 +252,16 @@ private fun ColumnScope.DisplayPage(
val columns by columnPreference.collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_library_columns),
max = 10,
value = columns,
valueRange = 0..10,
label = stringResource(MR.strings.pref_library_columns),
valueText = if (columns > 0) {
stringResource(MR.strings.pref_library_columns_per_row, columns)
columns.toString()
} else {
stringResource(MR.strings.label_default)
stringResource(MR.strings.label_auto)
},
onChange = columnPreference::set,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}

View File

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

View File

@ -21,13 +21,14 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import kotlinx.collections.immutable.persistentListOf
@ -40,6 +41,8 @@ import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.active
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun ChapterSettingsDialog(
@ -63,6 +66,8 @@ fun ChapterSettingsDialog(
)
}
val downloadedOnly = remember { Injekt.get<BasePreferences>().downloadedOnly().get() }
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = persistentListOf(
@ -97,7 +102,7 @@ fun ChapterSettingsDialog(
FilterPage(
downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged
.takeUnless { manga?.forceDownloaded() == true },
.takeUnless { downloadedOnly },
unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
onUnreadFilterChanged = onUnreadFilterChanged,
bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,

View File

@ -87,7 +87,7 @@ fun MangaScreen(
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -141,7 +141,7 @@ fun MangaScreen(
nextUpdate = nextUpdate,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked,
navigateUp = navigateUp,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -176,7 +176,7 @@ fun MangaScreen(
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
nextUpdate = nextUpdate,
onBackClicked = onBackClicked,
navigateUp = navigateUp,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onAddToLibraryClicked = onAddToLibraryClicked,
@ -214,7 +214,7 @@ private fun MangaScreenSmallImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -265,14 +265,13 @@ private fun MangaScreenSmallImpl(
)
}
val internalOnBackPressed = {
BackHandler(onBack = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
navigateUp()
}
}
BackHandler(onBack = internalOnBackPressed)
})
Scaffold(
topBar = {
@ -285,20 +284,18 @@ private fun MangaScreenSmallImpl(
val isFirstItemScrolled by remember {
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
}
val animatedTitleAlpha by animateFloatAsState(
val titleAlpha by animateFloatAsState(
if (!isFirstItemVisible) 1f else 0f,
label = "Top Bar Title",
)
val animatedBgAlpha by animateFloatAsState(
val backgroundAlpha by animateFloatAsState(
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
label = "Top Bar Background",
)
MangaToolbar(
title = state.manga.title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
navigateUp = navigateUp,
onClickFilter = onFilterClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
@ -306,8 +303,11 @@ private fun MangaScreenSmallImpl(
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
actionModeCounter = selectedChapterCount,
onCancelActionMode = { onAllChapterSelected(false) },
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { titleAlpha },
backgroundAlphaProvider = { backgroundAlpha },
)
},
bottomBar = {
@ -458,7 +458,7 @@ fun MangaScreenLargeImpl(
nextUpdate: Instant?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onAddToLibraryClicked: () -> Unit,
@ -515,14 +515,13 @@ fun MangaScreenLargeImpl(
val chapterListState = rememberLazyListState()
val internalOnBackPressed = {
BackHandler(onBack = {
if (isAnySelected) {
onAllChapterSelected(false)
} else {
onBackClicked()
navigateUp()
}
}
BackHandler(onBack = internalOnBackPressed)
})
Scaffold(
topBar = {
@ -532,19 +531,20 @@ fun MangaScreenLargeImpl(
MangaToolbar(
modifier = Modifier.onSizeChanged { topBarHeight = it.height },
title = state.manga.title,
titleAlphaProvider = { if (isAnySelected) 1f else 0f },
backgroundAlphaProvider = { 1f },
hasFilters = state.filterActive,
onBackClicked = internalOnBackPressed,
navigateUp = navigateUp,
onClickFilter = onFilterButtonClicked,
onClickShare = onShareClicked,
onClickDownload = onDownloadActionClicked,
onClickEditCategory = onEditCategoryClicked,
onClickRefresh = onRefresh,
onClickMigrate = onMigrateClicked,
onCancelActionMode = { onAllChapterSelected(false) },
actionModeCounter = selectedChapterCount,
onSelectAll = { onAllChapterSelected(true) },
onInvertSelection = { onInvertSelection() },
titleAlphaProvider = { 1f },
backgroundAlphaProvider = { 1f },
)
},
bottomBar = {

View File

@ -19,8 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR
@ -109,7 +108,7 @@ fun SetIntervalDialog(
}
Spacer(Modifier.height(MaterialTheme.padding.small))
if (onValueChanged != null && (isDevFlavor || isPreviewBuildType)) {
if (onValueChanged != null && (!isReleaseBuildType)) {
Text(stringResource(MR.strings.manga_interval_custom_amount))
BoxWithConstraints(

View File

@ -1,18 +1,12 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -20,12 +14,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.DownloadDropdownMenu
import eu.kanade.presentation.components.UpIcon
import eu.kanade.presentation.manga.DownloadAction
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
@ -35,9 +29,8 @@ import tachiyomi.presentation.core.theme.active
@Composable
fun MangaToolbar(
title: String,
titleAlphaProvider: () -> Float,
hasFilters: Boolean,
onBackClicked: () -> Unit,
navigateUp: () -> Unit,
onClickFilter: () -> Unit,
onClickShare: (() -> Unit)?,
onClickDownload: ((DownloadAction) -> Unit)?,
@ -47,118 +40,111 @@ fun MangaToolbar(
// For action mode
actionModeCounter: Int,
onCancelActionMode: () -> Unit,
onSelectAll: () -> Unit,
onInvertSelection: () -> Unit,
titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) {
Column(
val isActionMode = actionModeCounter > 0
AppBar(
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
} else {
AppBarTitle(title, modifier = Modifier.alpha(titleAlphaProvider()))
}
},
modifier = modifier,
) {
val isActionMode = actionModeCounter > 0
TopAppBar(
title = {
Text(
text = if (isActionMode) actionModeCounter.toString() else title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
navigateUp = navigateUp,
actions = {
var downloadExpanded by remember { mutableStateOf(false) }
if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
)
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
UpIcon(navigationIcon = Icons.Outlined.Close.takeIf { isActionMode })
}
},
actions = {
if (isActionMode) {
AppBarActions(
persistentListOf(
}
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
if (isActionMode) {
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
icon = Icons.Outlined.SelectAll,
onClick = onSelectAll,
),
)
add(
AppBar.Action(
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = onInvertSelection,
),
),
)
} else {
var downloadExpanded by remember { mutableStateOf(false) }
)
return@apply
}
if (onClickDownload != null) {
val onDismissRequest = { downloadExpanded = false }
DownloadDropdownMenu(
expanded = downloadExpanded,
onDismissRequest = onDismissRequest,
onDownloadClicked = onClickDownload,
add(
AppBar.Action(
title = stringResource(MR.strings.manga_download),
icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded },
),
)
}
val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
AppBarActions(
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
if (onClickDownload != null) {
add(
AppBar.Action(
title = stringResource(MR.strings.manga_download),
icon = Icons.Outlined.Download,
onClick = { downloadExpanded = !downloadExpanded },
),
)
}
add(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = filterTint,
onClick = onClickFilter,
),
)
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
onClick = onClickRefresh,
),
)
if (onClickEditCategory != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories),
onClick = onClickEditCategory,
),
)
}
if (onClickMigrate != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate),
onClick = onClickMigrate,
),
)
}
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = onClickShare,
),
)
}
}
.build(),
add(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = filterTint,
onClick = onClickFilter,
),
)
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
onClick = onClickRefresh,
),
)
if (onClickEditCategory != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_edit_categories),
onClick = onClickEditCategory,
),
)
}
if (onClickMigrate != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_migrate),
onClick = onClickMigrate,
),
)
}
if (onClickShare != null) {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.action_share),
onClick = onClickShare,
),
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()),
),
)
}
.build(),
)
},
isActionMode = isActionMode,
onCancelActionMode = onCancelActionMode,
)
}

View File

@ -1,12 +1,6 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.automirrored.outlined.Label
@ -40,7 +34,6 @@ fun MoreScreen(
onDownloadedOnlyChange: (Boolean) -> Unit,
incognitoMode: Boolean,
onIncognitoModeChange: (Boolean) -> Unit,
isFDroid: Boolean,
onClickDownloadQueue: () -> Unit,
onClickCategories: () -> Unit,
onClickStats: () -> Unit,
@ -50,19 +43,7 @@ fun MoreScreen(
) {
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
Column(
modifier = Modifier.windowInsetsPadding(
WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
),
) {
if (isFDroid) {
// Don't really care about slow updaters now
}
}
},
) { contentPadding ->
Scaffold { contentPadding ->
ScrollbarLazyColumn(
modifier = Modifier.padding(contentPadding),
) {

View File

@ -4,7 +4,6 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
@ -32,11 +31,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.analyticsIncluded
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@ -111,12 +112,14 @@ internal class PermissionStep : OnboardingStep {
onButtonClick = {
@SuppressLint("BatteryLife")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
},
)
if (!analyticsIncluded) return@Column
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more.settings
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
@ -17,7 +18,7 @@ sealed class Preference {
sealed class PreferenceItem<T> : Preference() {
abstract val subtitle: String?
abstract val icon: ImageVector?
abstract val onValueChanged: suspend (newValue: T) -> Boolean
abstract val onValueChanged: suspend (value: T) -> Boolean
/**
* A basic [PreferenceItem] that only displays texts.
@ -25,57 +26,58 @@ sealed class Preference {
data class TextPreference(
override val title: String,
override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val onClick: (() -> Unit)? = null,
) : PreferenceItem<String>()
) : PreferenceItem<String>() {
override val icon: ImageVector? = null
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
/**
* A [PreferenceItem] that provides a two-state toggleable option.
*/
data class SwitchPreference(
val pref: PreferenceData<Boolean>,
val preference: PreferenceData<Boolean>,
override val title: String,
override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>()
override val onValueChanged: suspend (value: Boolean) -> Boolean = { true },
) : PreferenceItem<Boolean>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] that provides a slider to select an integer number.
*/
data class SliderPreference(
val value: Int,
val min: Int = 0,
val max: Int,
override val title: String = "",
override val title: String,
val valueRange: IntProgression = 0..1,
@IntRange(from = 0) val steps: Int = with(valueRange) { (last - first) - 1 },
override val subtitle: String? = null,
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Int) -> Boolean = { true },
) : PreferenceItem<Int>()
override val onValueChanged: suspend (value: Int) -> Boolean = { true },
) : PreferenceItem<Int>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] that displays a list of entries as a dialog.
*/
@Suppress("UNCHECKED_CAST")
data class ListPreference<T>(
val pref: PreferenceData<T>,
val preference: PreferenceData<T>,
val entries: ImmutableMap<T, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: T, entries: ImmutableMap<T, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
val entries: ImmutableMap<T, String>,
override val onValueChanged: suspend (value: T) -> Boolean = { true },
) : PreferenceItem<T>() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
internal fun internalSet(value: Any) = preference.set(value as T)
internal suspend fun internalOnValueChanged(value: Any) = onValueChanged(value as T)
@Composable
internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap<out Any?, String>) =
@ -87,15 +89,14 @@ sealed class Preference {
*/
data class BasicListPreference(
val value: String,
val entries: ImmutableMap<String, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (value: String, entries: ImmutableMap<String, String>) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>()
/**
@ -103,52 +104,51 @@ sealed class Preference {
* Multiple entries can be selected at the same time.
*/
data class MultiSelectListPreference(
val pref: PreferenceData<Set<String>>,
val preference: PreferenceData<Set<String>>,
val entries: ImmutableMap<String, String>,
override val title: String,
override val subtitle: String? = "%s",
val subtitleProvider: @Composable (
value: Set<String>,
entries: ImmutableMap<String, String>,
) -> String? = { v, e ->
val combined = remember(v) {
v.map { e[it] }
.takeIf { it.isNotEmpty() }
?.joinToString()
} ?: stringResource(MR.strings.none)
subtitle?.format(combined)
},
val subtitleProvider: @Composable (value: Set<String>, entries: ImmutableMap<String, String>) -> String? =
{ v, e ->
val combined = remember(v, e) {
v.mapNotNull { e[it] }
.joinToString()
.takeUnless { it.isBlank() }
}
?: stringResource(MR.strings.none)
subtitle?.format(combined)
},
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
val entries: ImmutableMap<String, String>,
override val onValueChanged: suspend (value: Set<String>) -> Boolean = { true },
) : PreferenceItem<Set<String>>()
/**
* A [PreferenceItem] that shows a EditText in the dialog.
*/
data class EditTextPreference(
val pref: PreferenceData<String>,
val preference: PreferenceData<String>,
override val title: String,
override val subtitle: String? = "%s",
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
) : PreferenceItem<String>()
override val onValueChanged: suspend (value: String) -> Boolean = { true },
) : PreferenceItem<String>() {
override val icon: ImageVector? = null
}
/**
* A [PreferenceItem] for individual tracker.
*/
data class TrackerPreference(
val tracker: Tracker,
override val title: String,
val login: () -> Unit,
val logout: () -> Unit,
) : PreferenceItem<String>() {
override val title: String = ""
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
data class InfoPreference(
@ -157,17 +157,17 @@ sealed class Preference {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (value: String) -> Boolean = { true }
}
data class CustomPreference(
override val title: String,
val content: @Composable (PreferenceItem<String>) -> Unit,
) : PreferenceItem<String>() {
val content: @Composable () -> Unit,
) : PreferenceItem<Unit>() {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (value: Unit) -> Boolean = { true }
}
}

View File

@ -5,6 +5,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
@ -12,16 +14,20 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.InfoWidget
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.more.settings.widget.PrefsVerticalPadding
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TitleFontSize
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.BaseSliderItem
import tachiyomi.presentation.core.util.collectAsState
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
@ -60,7 +66,7 @@ internal fun PreferenceItem(
) {
when (item) {
is Preference.PreferenceItem.SwitchPreference -> {
val value by item.pref.collectAsState()
val value by item.preference.collectAsState()
SwitchPreferenceWidget(
title = item.title,
subtitle = item.subtitle,
@ -69,29 +75,33 @@ internal fun PreferenceItem(
onCheckedChanged = { newValue ->
scope.launch {
if (item.onValueChanged(newValue)) {
item.pref.set(newValue)
item.preference.set(newValue)
}
}
},
)
}
is Preference.PreferenceItem.SliderPreference -> {
// TODO: use different composable?
SliderItem(
BaseSliderItem(
label = item.title,
min = item.min,
max = item.max,
value = item.value,
valueRange = item.valueRange,
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
steps = item.steps,
labelStyle = MaterialTheme.typography.titleLarge.copy(fontSize = TitleFontSize),
onChange = {
scope.launch {
item.onValueChanged(it)
}
},
modifier = Modifier.padding(
horizontal = PrefsHorizontalPadding,
vertical = PrefsVerticalPadding,
),
)
}
is Preference.PreferenceItem.ListPreference<*> -> {
val value by item.pref.collectAsState()
val value by item.preference.collectAsState()
ListPreferenceWidget(
value = value,
title = item.title,
@ -118,14 +128,14 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.MultiSelectListPreference -> {
val values by item.pref.collectAsState()
val values by item.preference.collectAsState()
MultiSelectListPreferenceWidget(
preference = item,
values = values,
onValuesChange = { newValues ->
scope.launch {
if (item.onValueChanged(newValues)) {
item.pref.set(newValues.toMutableSet())
item.preference.set(newValues.toMutableSet())
}
}
},
@ -140,7 +150,7 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.EditTextPreference -> {
val values by item.pref.collectAsState()
val values by item.preference.collectAsState()
EditTextPreferenceWidget(
title = item.title,
subtitle = item.subtitle,
@ -148,7 +158,7 @@ internal fun PreferenceItem(
value = values,
onConfirm = {
val accepted = item.onValueChanged(it)
if (accepted) item.pref.set(it)
if (accepted) item.preference.set(it)
accepted
},
)
@ -167,7 +177,7 @@ internal fun PreferenceItem(
InfoWidget(text = item.title)
}
is Preference.PreferenceItem.CustomPreference -> {
item.content(item)
item.content()
}
}
}

View File

@ -47,8 +47,8 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
@ -61,6 +61,7 @@ import logcat.LogPriority
import okhttp3.Headers
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.i18n.MR
@ -96,7 +97,7 @@ object SettingsAdvancedScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = networkPreferences.verboseLogging(),
preference = networkPreferences.verboseLogging(),
title = stringResource(MR.strings.pref_verbose_logging),
subtitle = stringResource(MR.strings.pref_verbose_logging_summary),
onValueChanged = {
@ -235,8 +236,7 @@ object SettingsAdvancedScreen : SearchableSettings {
},
),
Preference.PreferenceItem.ListPreference(
pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
preference = networkPreferences.dohProvider(),
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare",
@ -252,13 +252,14 @@ object SettingsAdvancedScreen : SearchableSettings {
PREF_DOH_NJALLA to "Njalla",
PREF_DOH_SHECAN to "Shecan",
),
title = stringResource(MR.strings.pref_dns_over_https),
onValueChanged = {
context.toast(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.EditTextPreference(
pref = userAgentPref,
preference = userAgentPref,
title = stringResource(MR.strings.pref_user_agent_string),
onValueChanged = {
try {
@ -334,6 +335,31 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
preference = basePreferences.hardwareBitmapThreshold(),
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(),
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,
),
Preference.PreferenceItem.SwitchPreference(
preference = basePreferences.alwaysDecodeLongStripWithSSIV(),
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(),
@ -382,19 +408,19 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.label_extensions),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
preference = extensionInstallerPref,
entries = extensionInstallerPref.entries
.filter {
// TODO: allow private option in stable versions once URL handling is more fleshed out
if (isPreviewBuildType || isDevFlavor) {
true
} else {
if (isReleaseBuildType) {
it != BasePreferences.ExtensionInstaller.PRIVATE
} else {
true
}
}
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.ext_installer_pref),
onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled

View File

@ -82,7 +82,7 @@ object SettingsAppearanceScreen : SearchableSettings {
}
},
Preference.PreferenceItem.SwitchPreference(
pref = amoledPref,
preference = amoledPref,
title = stringResource(MR.strings.pref_dark_theme_pure_black),
enabled = themeMode != ThemeMode.LIGHT,
onValueChanged = {
@ -116,28 +116,28 @@ object SettingsAppearanceScreen : SearchableSettings {
onClick = { navigator.push(AppLanguageScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
preference = uiPreferences.tabletUiMode(),
entries = TabletUiMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
onValueChanged = {
context.toast(MR.strings.requires_app_restart)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(),
title = stringResource(MR.strings.pref_date_format),
preference = uiPreferences.dateFormat(),
entries = DateFormats
.associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
}
.toImmutableMap(),
title = stringResource(MR.strings.pref_date_format),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(),
preference = uiPreferences.relativeTime(),
title = stringResource(MR.strings.pref_relative_format),
subtitle = stringResource(
MR.strings.pref_relative_format_summary,

View File

@ -43,7 +43,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_sources),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInLibraryItems(),
preference = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items),
),
Preference.PreferenceItem.TextPreference(
@ -59,7 +59,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(),
preference = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = {

View File

@ -7,7 +7,9 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
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.material.icons.Icons
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.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
@ -22,12 +26,15 @@ import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@ -45,10 +52,14 @@ import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.export.LibraryExporter
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
@ -57,8 +68,11 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
@ -95,6 +109,7 @@ object SettingsDataScreen : SearchableSettings {
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(),
getExportGroup(),
)
}
@ -239,8 +254,7 @@ object SettingsDataScreen : SearchableSettings {
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
preference = backupPreferences.backupInterval(),
entries = persistentMapOf(
0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour),
@ -249,6 +263,7 @@ object SettingsDataScreen : SearchableSettings {
48 to stringResource(MR.strings.update_48hour),
168 to stringResource(MR.strings.update_weekly),
),
title = stringResource(MR.strings.pref_backup_interval),
onValueChanged = {
BackupCreateJob.setupTask(context, it)
true
@ -306,10 +321,147 @@ object SettingsDataScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoClearChapterCache(),
preference = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
),
),
)
}
@Composable
private fun getExportGroup(): Preference.PreferenceGroup {
var showDialog by remember { mutableStateOf(false) }
var exportOptions by remember {
mutableStateOf(
ExportOptions(
includeTitle = true,
includeAuthor = true,
includeArtist = true,
),
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val getFavorites = remember { Injekt.get<GetFavorites>() }
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
LaunchedEffect(Unit) {
favorites = getFavorites.await()
}
val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/csv"),
) { uri ->
uri?.let {
scope.launch {
LibraryExporter.exportToCsv(
context = context,
uri = it,
favorites = favorites,
options = exportOptions,
onExportComplete = {
scope.launch(Dispatchers.Main) {
context.toast(MR.strings.library_exported)
}
},
)
}
}
}
if (showDialog) {
ColumnSelectionDialog(
options = exportOptions,
onConfirm = { options ->
exportOptions = options
saveFileLauncher.launch("mihon_library.csv")
},
onDismissRequest = { showDialog = false },
)
}
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(
options: ExportOptions,
onConfirm: (ExportOptions) -> Unit,
onDismissRequest: () -> Unit,
) {
var titleSelected by remember { mutableStateOf(options.includeTitle) }
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
var artistSelected by remember { mutableStateOf(options.includeArtist) }
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(
ExportOptions(
includeTitle = titleSelected,
includeAuthor = authorSelected,
includeArtist = artistSelected,
),
)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
}

View File

@ -39,15 +39,15 @@ object SettingsDownloadScreen : SearchableSettings {
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(),
preference = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.saveChaptersAsCBZ(),
preference = downloadPreferences.saveChaptersAsCBZ(),
title = stringResource(MR.strings.save_chapter_as_cbz),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.splitTallImages(),
preference = downloadPreferences.splitTallImages(),
title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary),
),
@ -72,12 +72,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_delete_chapters),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(),
preference = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
),
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
preference = downloadPreferences.removeAfterReadSlots(),
entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter),
@ -86,9 +85,10 @@ object SettingsDownloadScreen : SearchableSettings {
3 to stringResource(MR.strings.fourth_to_last),
4 to stringResource(MR.strings.fifth_to_last),
),
title = stringResource(MR.strings.pref_remove_after_read),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeBookmarkedChapters(),
preference = downloadPreferences.removeBookmarkedChapters(),
title = stringResource(MR.strings.pref_remove_bookmarked_chapters),
),
getExcludedCategoriesPreference(
@ -105,11 +105,11 @@ object SettingsDownloadScreen : SearchableSettings {
categories: () -> List<Category>,
): Preference.PreferenceItem.MultiSelectListPreference {
return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
preference = downloadPreferences.removeExcludeCategories(),
entries = categories()
.associate { it.id.toString() to it.visualName }
.toImmutableMap(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
)
}
@ -149,11 +149,11 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_auto_download),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewChaptersPref,
preference = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new),
),
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewUnreadChaptersOnlyPref,
preference = downloadNewUnreadChaptersOnlyPref,
title = stringResource(MR.strings.pref_download_new_unread_chapters_only),
enabled = downloadNewChapters,
),
@ -164,8 +164,8 @@ object SettingsDownloadScreen : SearchableSettings {
included = included,
excluded = excluded,
),
onClick = { showDialog = true },
enabled = downloadNewChapters,
onClick = { showDialog = true },
),
),
)
@ -179,8 +179,7 @@ object SettingsDownloadScreen : SearchableSettings {
title = stringResource(MR.strings.download_ahead),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
preference = downloadPreferences.autoDownloadWhileReading(),
entries = listOf(0, 2, 3, 5, 10)
.associateWith {
if (it == 0) {
@ -190,6 +189,7 @@ object SettingsDownloadScreen : SearchableSettings {
}
}
.toImmutableMap(),
title = stringResource(MR.strings.auto_download_while_reading),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
),

View File

@ -89,12 +89,12 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { navigator.push(CategoryScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.defaultCategory(),
title = stringResource(MR.strings.default_category),
preference = libraryPreferences.defaultCategory(),
entries = ids.zip(labels).toMap().toImmutableMap(),
title = stringResource(MR.strings.default_category),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(),
preference = libraryPreferences.categorizedDisplaySettings(),
title = stringResource(MR.strings.categorized_display_settings),
onValueChanged = {
if (!it) {
@ -146,8 +146,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_library_update),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
preference = autoUpdateIntervalPref,
entries = persistentMapOf(
0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour),
@ -156,21 +155,22 @@ object SettingsLibraryScreen : SearchableSettings {
72 to stringResource(MR.strings.update_72hour),
168 to stringResource(MR.strings.update_weekly),
),
title = stringResource(MR.strings.pref_library_update_interval),
onValueChanged = {
LibraryUpdateJob.setupTask(context, it)
true
},
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateDeviceRestrictions(),
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
preference = libraryPreferences.autoUpdateDeviceRestrictions(),
entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging),
),
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
enabled = autoUpdateInterval > 0,
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
@ -187,22 +187,22 @@ object SettingsLibraryScreen : SearchableSettings {
onClick = { showCategoriesDialog = true },
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateMetadata(),
preference = libraryPreferences.autoUpdateMetadata(),
title = stringResource(MR.strings.pref_library_update_refresh_metadata),
subtitle = stringResource(MR.strings.pref_library_update_refresh_metadata_summary),
),
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_smart_update),
preference = libraryPreferences.autoUpdateMangaRestrictions(),
entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(MR.strings.pref_update_only_in_release_period),
),
title = stringResource(MR.strings.pref_library_update_smart_update),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.newShowUpdatesCount(),
preference = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
),
),
@ -217,8 +217,7 @@ object SettingsLibraryScreen : SearchableSettings {
title = stringResource(MR.strings.pref_chapter_swipe),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
preference = libraryPreferences.swipeToStartAction(),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
@ -229,10 +228,10 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
title = stringResource(MR.strings.pref_chapter_swipe_start),
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
preference = libraryPreferences.swipeToEndAction(),
entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
@ -243,6 +242,7 @@ object SettingsLibraryScreen : SearchableSettings {
LibraryPreferences.ChapterSwipeAction.Download to
stringResource(MR.strings.action_download),
),
title = stringResource(MR.strings.pref_chapter_swipe_end),
),
),
)

View File

@ -33,33 +33,33 @@ object SettingsReaderScreen : SearchableSettings {
return listOf(
Preference.PreferenceItem.ListPreference(
pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
preference = readerPref.defaultReadingMode(),
entries = ReadingMode.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
preference = readerPref.doubleTapAnimSpeed(),
entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showReadingMode(),
preference = readerPref.showReadingMode(),
title = stringResource(MR.strings.pref_show_reading_mode),
subtitle = stringResource(MR.strings.pref_show_reading_mode_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.showNavigationOverlayOnStart(),
preference = readerPref.showNavigationOverlayOnStart(),
title = stringResource(MR.strings.pref_show_navigation_mode),
subtitle = stringResource(MR.strings.pref_show_navigation_mode_summary),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPref.pageTransitions(),
preference = readerPref.pageTransitions(),
title = stringResource(MR.strings.pref_page_transitions),
),
getDisplayGroup(readerPreferences = readerPref),
@ -80,39 +80,39 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
preference = readerPreferences.defaultOrientationType(),
entries = ReaderOrientation.entries.drop(1)
.associate { it.flagValue to stringResource(it.stringRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_rotation_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
preference = readerPreferences.readerTheme(),
entries = persistentMapOf(
1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background),
3 to stringResource(MR.strings.automatic_background),
),
title = stringResource(MR.strings.pref_reader_theme),
),
Preference.PreferenceItem.SwitchPreference(
pref = fullscreenPref,
preference = fullscreenPref,
title = stringResource(MR.strings.pref_fullscreen),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cutoutShort(),
preference = readerPreferences.cutoutShort(),
title = stringResource(MR.strings.pref_cutout_short),
enabled = fullscreen &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.keepScreenOn(),
preference = readerPreferences.keepScreenOn(),
title = stringResource(MR.strings.pref_keep_screen_on),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.showPageNumber(),
preference = readerPreferences.showPageNumber(),
title = stringResource(MR.strings.pref_show_page_number),
),
),
@ -135,43 +135,41 @@ object SettingsReaderScreen : SearchableSettings {
title = "E-Ink",
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.flashOnPageChange(),
preference = readerPreferences.flashOnPageChange(),
title = stringResource(MR.strings.pref_flash_page),
subtitle = stringResource(MR.strings.pref_flash_page_summ),
),
Preference.PreferenceItem.SliderPreference(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
min = 1,
max = 15,
valueRange = 1..15,
title = stringResource(MR.strings.pref_flash_duration),
subtitle = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
enabled = flashPageState,
onValueChanged = {
flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.SliderPreference(
value = flashInterval,
min = 1,
max = 10,
valueRange = 1..10,
title = stringResource(MR.strings.pref_flash_page_interval),
subtitle = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
enabled = flashPageState,
onValueChanged = {
flashIntervalPref.set(it)
true
},
enabled = flashPageState,
),
Preference.PreferenceItem.ListPreference(
pref = flashColorPref,
title = stringResource(MR.strings.pref_flash_with),
preference = flashColorPref,
entries = persistentMapOf(
ReaderPreferences.FlashColor.BLACK to stringResource(MR.strings.pref_flash_style_black),
ReaderPreferences.FlashColor.WHITE to stringResource(MR.strings.pref_flash_style_white),
ReaderPreferences.FlashColor.WHITE_BLACK
to stringResource(MR.strings.pref_flash_style_white_black),
),
title = stringResource(MR.strings.pref_flash_with),
enabled = flashPageState,
),
),
@ -184,21 +182,25 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_category_reading),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(),
preference = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipFiltered(),
preference = readerPreferences.skipFiltered(),
title = stringResource(MR.strings.pref_skip_filtered_chapters),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipDupe(),
preference = readerPreferences.skipDupe(),
title = stringResource(MR.strings.pref_skip_dupe_chapters),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.alwaysShowChapterTransition(),
preference = readerPreferences.alwaysShowChapterTransition(),
title = stringResource(MR.strings.pref_always_show_chapter_transition),
),
Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.markDuplicateReadChapterAsRead(),
title = stringResource(MR.strings.pref_mark_duplicate_read_chapter_read),
),
),
)
}
@ -219,16 +221,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pager_viewer),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
preference = navModePref,
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
preference = readerPreferences.pagerNavInverted(),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
@ -237,40 +238,41 @@ object SettingsReaderScreen : SearchableSettings {
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
pref = imageScaleTypePref,
title = stringResource(MR.strings.pref_image_scale_type),
preference = imageScaleTypePref,
entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_image_scale_type),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
preference = readerPreferences.zoomStart(),
entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_zoom_start),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(),
preference = readerPreferences.cropBorders(),
title = stringResource(MR.strings.pref_crop_borders),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.landscapeZoom(),
preference = readerPreferences.landscapeZoom(),
title = stringResource(MR.strings.pref_landscape_zoom),
enabled = imageScaleType == 1,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.navigateToPan(),
preference = readerPreferences.navigateToPan(),
title = stringResource(MR.strings.pref_navigate_pan),
enabled = navMode != 5,
),
Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref,
preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = {
rotateToFitPref.set(false)
@ -278,13 +280,13 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertPaged(),
preference = readerPreferences.dualPageInvertPaged(),
title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref,
preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = {
dualPageSplitPref.set(false)
@ -292,7 +294,7 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvert(),
preference = readerPreferences.dualPageRotateToFitInvert(),
title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit,
),
@ -318,16 +320,15 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.webtoon_viewer),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
preference = navModePref,
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
.toMap()
.toImmutableMap(),
title = stringResource(MR.strings.pref_viewer_nav),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
preference = readerPreferences.webtoonNavInverted(),
entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
@ -336,35 +337,37 @@ object SettingsReaderScreen : SearchableSettings {
)
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
enabled = navMode != 5,
),
Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding,
valueRange = ReaderPreferences.let {
it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX
},
title = stringResource(MR.strings.pref_webtoon_side_padding),
subtitle = numberFormat.format(webtoonSidePadding / 100f),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = {
webtoonSidePaddingPref.set(it)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
preference = readerPreferences.readerHideThreshold(),
entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
ReaderPreferences.ReaderHideThreshold.LOWEST to stringResource(MR.strings.pref_lowest),
),
title = stringResource(MR.strings.pref_hide_threshold),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBordersWebtoon(),
preference = readerPreferences.cropBordersWebtoon(),
title = stringResource(MR.strings.pref_crop_borders),
),
Preference.PreferenceItem.SwitchPreference(
pref = dualPageSplitPref,
preference = dualPageSplitPref,
title = stringResource(MR.strings.pref_dual_page_split),
onValueChanged = {
rotateToFitPref.set(false)
@ -372,13 +375,13 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageInvertWebtoon(),
preference = readerPreferences.dualPageInvertWebtoon(),
title = stringResource(MR.strings.pref_dual_page_invert),
subtitle = stringResource(MR.strings.pref_dual_page_invert_summary),
enabled = dualPageSplit,
),
Preference.PreferenceItem.SwitchPreference(
pref = rotateToFitPref,
preference = rotateToFitPref,
title = stringResource(MR.strings.pref_page_rotate),
onValueChanged = {
dualPageSplitPref.set(false)
@ -386,16 +389,16 @@ object SettingsReaderScreen : SearchableSettings {
},
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.dualPageRotateToFitInvertWebtoon(),
preference = readerPreferences.dualPageRotateToFitInvertWebtoon(),
title = stringResource(MR.strings.pref_page_rotate_invert),
enabled = rotateToFit,
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
preference = readerPreferences.webtoonDoubleTapZoomEnabled(),
title = stringResource(MR.strings.pref_double_tap_zoom),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.webtoonDisableZoomOut(),
preference = readerPreferences.webtoonDisableZoomOut(),
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
),
),
@ -410,11 +413,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_navigation),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref,
preference = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithVolumeKeysInverted(),
preference = readerPreferences.readWithVolumeKeysInverted(),
title = stringResource(MR.strings.pref_read_with_volume_keys_inverted),
enabled = readWithVolumeKeys,
),
@ -428,11 +431,11 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_reader_actions),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(),
preference = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.folderPerManga(),
preference = readerPreferences.folderPerManga(),
title = stringResource(MR.strings.pref_create_folder_per_manga),
subtitle = stringResource(MR.strings.pref_create_folder_per_manga_summary),
),

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.analyticsIncluded
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.common.i18n.stringResource
@ -31,10 +32,11 @@ object SettingsSecurityScreen : SearchableSettings {
override fun getPreferences(): List<Preference> {
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return listOf(
getSecurityGroup(securityPreferences),
getFirebaseGroup(privacyPreferences),
)
return buildList(2) {
add(getSecurityGroup(securityPreferences))
if (!analyticsIncluded) return@buildList
add(getFirebaseGroup(privacyPreferences))
}
}
@Composable
@ -50,7 +52,7 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
preference = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics),
enabled = authSupported,
onValueChanged = {
@ -60,9 +62,7 @@ object SettingsSecurityScreen : SearchableSettings {
},
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.lockAppAfter(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
preference = securityPreferences.lockAppAfter(),
entries = LockAfterValues
.associateWith {
when (it) {
@ -72,6 +72,8 @@ object SettingsSecurityScreen : SearchableSettings {
}
}
.toImmutableMap(),
title = stringResource(MR.strings.lock_when_idle),
enabled = authSupported && useAuth,
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
@ -80,15 +82,15 @@ object SettingsSecurityScreen : SearchableSettings {
),
Preference.PreferenceItem.SwitchPreference(
pref = securityPreferences.hideNotificationContent(),
preference = securityPreferences.hideNotificationContent(),
title = stringResource(MR.strings.hide_notification_content),
),
Preference.PreferenceItem.ListPreference(
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
preference = securityPreferences.secureScreen(),
entries = SecurityPreferences.SecureScreenMode.entries
.associateWith { stringResource(it.titleRes) }
.toImmutableMap(),
title = stringResource(MR.strings.secure_screen),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
),
@ -103,12 +105,12 @@ object SettingsSecurityScreen : SearchableSettings {
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
preference = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
preference = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),

View File

@ -125,51 +125,45 @@ object SettingsTrackingScreen : SearchableSettings {
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = trackPreferences.autoUpdateTrack(),
preference = trackPreferences.autoUpdateTrack(),
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),
preference = trackPreferences.autoUpdateTrackOnMarkRead(),
entries = AutoTrackState.entries
.associateWith { stringResource(it.titleRes) }
.toPersistentMap(),
title = stringResource(MR.strings.pref_auto_update_manga_on_mark_read),
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.services),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, MR.strings.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, MR.strings.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
@ -183,7 +177,6 @@ object SettingsTrackingScreen : SearchableSettings {
enhancedTrackers.first
.map { service ->
Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service,
login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout,

View File

@ -35,7 +35,9 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.updaterEnabled
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withIOContext
@ -97,7 +99,7 @@ object AboutScreen : Screen() {
)
}
if (BuildConfig.INCLUDE_UPDATER) {
if (updaterEnabled) {
item {
TextPreferenceWidget(
title = stringResource(MR.strings.check_for_updates),
@ -245,7 +247,7 @@ object AboutScreen : Screen() {
}
}
}
BuildConfig.PREVIEW -> {
isPreviewBuildType -> {
"Beta r${BuildConfig.COMMIT_COUNT}".let {
if (withBuildDate) {
"$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -36,12 +37,12 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue,
valueText = customBrightnessValue.toString(),
valueRange = -75..100,
steps = 0,
label = stringResource(MR.strings.pref_custom_brightness),
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
}
@ -53,48 +54,52 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem(
label = stringResource(MR.strings.color_filter_r_value),
max = 255,
value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_r_value),
onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_g_value),
max = 255,
value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_g_value),
onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_b_value),
max = 255,
value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_b_value),
onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
label = stringResource(MR.strings.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(),
valueRange = 0..255,
steps = 0,
label = stringResource(MR.strings.color_filter_a_value),
onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24)
}
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -97,21 +98,21 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
if (flashPageState) {
SliderItem(
value = flashMillis / ReaderPreferences.MILLI_CONVERSION,
valueRange = 1..15,
label = stringResource(MR.strings.pref_flash_duration),
valueText = stringResource(MR.strings.pref_flash_duration_summary, flashMillis),
onChange = { flashMillisPref.set(it * ReaderPreferences.MILLI_CONVERSION) },
min = 1,
max = 15,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SliderItem(
value = flashInterval,
valueRange = 1..10,
label = stringResource(MR.strings.pref_flash_page_interval),
valueText = pluralStringResource(MR.plurals.pref_pages, flashInterval, flashInterval),
onChange = {
flashIntervalPref.set(it)
},
min = 1,
max = 10,
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
SettingsChipRow(MR.strings.pref_flash_with) {
flashColors.map { (labelRes, value) ->

View File

@ -2,6 +2,7 @@ package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -152,14 +153,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding,
valueRange = ReaderPreferences.let { it.WEBTOON_PADDING_MIN..it.WEBTOON_PADDING_MAX },
label = stringResource(MR.strings.pref_webtoon_side_padding),
valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = {
screenModel.preferences.webtoonSidePadding().set(it)
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
CheckboxItem(

View File

@ -13,6 +13,7 @@ import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
import eu.kanade.presentation.theme.colorscheme.MonetColorScheme
import eu.kanade.presentation.theme.colorscheme.MonochromeColorScheme
import eu.kanade.presentation.theme.colorscheme.NordColorScheme
import eu.kanade.presentation.theme.colorscheme.StrawberryColorScheme
import eu.kanade.presentation.theme.colorscheme.TachiyomiColorScheme
@ -79,6 +80,7 @@ private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
AppTheme.LAVENDER to LavenderColorScheme,
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
AppTheme.MONOCHROME to MonochromeColorScheme,
AppTheme.NORD to NordColorScheme,
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
AppTheme.TAKO to TakoColorScheme,

View File

@ -0,0 +1,84 @@
package eu.kanade.presentation.theme.colorscheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
internal object MonochromeColorScheme : BaseColorScheme() {
override val darkScheme = darkColorScheme(
primary = Color(0xFFFFFFFF),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFFFFFFFF),
onPrimaryContainer = Color(0xFF000000),
secondary = Color(0xFFFFFFFF),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFF777777),
onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFF777777),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFFFFF),
onTertiaryContainer = Color(0xFF000000),
error = Color(0xFFFFFFFF),
onError = Color(0xFF000000),
errorContainer = Color(0xFFFFFFFF),
onErrorContainer = Color(0xFF000000),
background = Color(0xFF000000),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF000000),
onSurface = Color(0xFFFFFFFF),
surfaceVariant = Color(0xFF000000),
onSurfaceVariant = Color(0xFFFFFFFF),
outline = Color(0xFFFFFFFF),
outlineVariant = Color(0xFFFFFFFF),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFFFFFFFF),
inverseOnSurface = Color(0xFF000000),
inversePrimary = Color(0xFF000000),
surfaceDim = Color(0xFF000000),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFF000000),
surfaceContainerLow = Color(0xFF000000),
surfaceContainer = Color(0xFF000000),
surfaceContainerHigh = Color(0xFF000000),
surfaceContainerHighest = Color(0xFF000000),
)
override val lightScheme = lightColorScheme(
primary = Color(0xFF000000),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF000000),
onPrimaryContainer = Color(0xFFFFFFFF),
secondary = Color(0xFF000000),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFF888888),
onSecondaryContainer = Color(0xFFFFFFFF),
tertiary = Color(0xFF888888),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFF000000),
onTertiaryContainer = Color(0xFFFFFFFF),
error = Color(0xFF000000),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFF000000),
onErrorContainer = Color(0xFFFFFFFF),
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF000000),
surface = Color(0xFFFFFFFF),
onSurface = Color(0xFF000000),
surfaceVariant = Color(0xFFFFFFFF),
onSurfaceVariant = Color(0xFF000000),
outline = Color(0xFF000000),
outlineVariant = Color(0xFF000000),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFF000000),
inverseOnSurface = Color(0xFFFFFFFF),
inversePrimary = Color(0xFFFFFFFF),
surfaceDim = Color(0xFFFFFFFF),
surfaceBright = Color(0xFFFFFFFF),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFFFFFFF),
surfaceContainer = Color(0xFFFFFFFF),
surfaceContainerHigh = Color(0xFFFFFFFF),
surfaceContainerHighest = Color(0xFFFFFFFF),
)
}

View File

@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit,
onTogglePrivate: (TrackItem) -> Unit,
) {
Column(
modifier = Modifier
@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
if (item.track != null) {
val supportsScoring = item.tracker.getScoreList().isNotEmpty()
val supportsReadingDates = item.tracker.supportsReadingDates
val supportsPrivate = item.tracker.supportsPrivateTracking
TrackInfoItem(
title = item.track.title,
tracker = item.tracker,
@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) },
private = item.track.private,
onTogglePrivate = { onTogglePrivate(item) }
.takeIf { supportsPrivate },
)
} else {
TrackInfoItemEmpty(
@ -144,17 +154,37 @@ private fun TrackInfoItem(
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) {
val context = LocalContext.current
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
BadgedBox(
badge = {
if (private) {
Badge(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.absoluteOffset(x = (-5).dp),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.tracked_privately),
modifier = Modifier.size(14.dp),
)
}
}
},
) {
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
}
Box(
modifier = Modifier
.height(48.dp)
@ -181,6 +211,8 @@ private fun TrackInfoItem(
onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved,
onCopyLink = onCopyLink,
private = private,
onTogglePrivate = onTogglePrivate,
)
}
@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit,
onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
expanded = false
},
)
if (onTogglePrivate != null) {
DropdownMenuItem(
text = {
Text(
stringResource(
if (private) {
MR.strings.action_toggle_private_off
} else {
MR.strings.action_toggle_private_on
},
),
)
},
onClick = {
onTogglePrivate()
expanded = false
},
)
}
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_remove)) },
onClick = {

View File

@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
remoteUrl = "https://example.com",
startDate = 0L,
finishDate = 0L,
private = false,
)
private val privateTrack = aTrack.copy(private = true)
private val trackItemWithoutTrack = TrackItem(
track = null,
tracker = DummyTracker(
@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
name = "Example Tracker 2",
),
)
private val trackItemWithPrivateTrack = TrackItem(
track = privateTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome(
@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
private val trackerWithPrivateTracking = @Composable {
TrackInfoDialogHome(
trackItems = listOf(trackItemWithPrivateTrack),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
)
}
@ -80,5 +108,6 @@ internal class TrackInfoDialogHomePreviewProvider :
get() = sequenceOf(
trackersWithAndWithoutTrack,
noTrackers,
trackerWithPrivateTracking,
)
}

View File

@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
@ -90,8 +91,9 @@ fun TrackerSearch(
queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?,
onSelectedChange: (TrackSearch) -> Unit,
onConfirmSelection: () -> Unit,
onConfirmSelection: (private: Boolean) -> Unit,
onDismissRequest: () -> Unit,
supportsPrivateTracking: Boolean,
) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
@ -164,15 +166,31 @@ fun TrackerSearch(
enter = fadeIn() + slideInVertically { it / 2 },
exit = slideOutVertically { it / 2 } + fadeOut(),
) {
Button(
onClick = { onConfirmSelection() },
Row(
modifier = Modifier
.padding(12.dp)
.padding(MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(),
elevation = ButtonDefaults.elevatedButtonElevation(),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.action_track))
Button(
onClick = { onConfirmSelection(false) },
modifier = Modifier.weight(1f),
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Text(text = stringResource(MR.strings.action_track))
}
if (supportsPrivateTracking) {
Button(
onClick = { onConfirmSelection(true) },
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.action_toggle_private_on),
)
}
}
}
}
},
@ -286,6 +304,15 @@ private fun SearchResultItem(
}
},
)
if (trackSearch.authors.isNotEmpty() || trackSearch.artists.isNotEmpty()) {
Text(
text = (trackSearch.authors + trackSearch.artists).distinct().joinToString(),
modifier = Modifier.secondaryItemAlpha(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
if (type.isNotBlank()) {
SearchResultItemDetails(
title = stringResource(MR.strings.track_type),

View File

@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
import kotlin.random.Random
internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composable () -> Unit> {
@ -20,6 +23,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithoutSelected = @Composable {
@ -31,6 +35,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val loading = @Composable {
@ -42,12 +47,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithPrivateTracking = @Composable {
val items = someTrackSearches().take(30).toList()
TrackerSearch(
state = TextFieldState(initialText = "search text"),
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = true,
)
}
override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected,
fullPageWithoutSelected,
loading,
fullPageWithPrivateTracking,
)
private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
@ -56,6 +76,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
}
}
private val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong()
it.manga_id = Random.nextLong()
@ -71,11 +93,17 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
it.finished_reading_date = 0L
it.tracking_url = "https://example.com/tracker-example"
it.cover_url = "https://example.com/cover.png"
it.start_date = Instant.now().minus((1L..365).random(), ChronoUnit.DAYS).toString()
it.start_date = formatter.format(Date.from(Instant.now().minus((1L..365).random(), ChronoUnit.DAYS)))
it.summary = lorem((0..40).random()).joinToString()
it.publishing_status = if (Random.nextBoolean()) "Finished" else ""
it.publishing_type = if (Random.nextBoolean()) "Oneshot" else ""
it.artists = randomNames()
it.authors = randomNames()
it
}
private fun randomNames(): List<String> = (0..(0..3).random()).map { lorem((3..5).random()).joinToString() }
private fun lorem(words: Int): Sequence<String> =
LoremIpsum(words).values
}

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import com.kevinnzou.web.AccompanistWebViewClient
@ -37,13 +39,18 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import okhttp3.Request
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable
fun WebViewScreenContent(
@ -58,8 +65,11 @@ fun WebViewScreenContent(
) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator()
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
val network = remember { Injekt.get<NetworkHelper>() }
val spoofedPackageName = remember { WebViewUtil.spoofedPackageName(context) }
var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) }
@ -114,6 +124,40 @@ fun WebViewScreenContent(
}
return super.shouldOverrideUrlLoading(view, request)
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
return try {
val internalRequest = Request.Builder().apply {
url(request!!.url.toString())
request.requestHeaders.forEach { (key, value) ->
if (key == "X-Requested-With" && value in setOf(context.packageName, spoofedPackageName)) {
return@forEach
}
addHeader(key, value)
}
method(request.method, null)
}.build()
val response = network.nonCloudflareClient.newCall(internalRequest).execute()
val contentType = response.body.contentType()?.let { "${it.type}/${it.subtype}" } ?: "text/html"
val contentEncoding = response.body.contentType()?.charset()?.name() ?: "utf-8"
WebResourceResponse(
contentType,
contentEncoding,
response.code,
response.message,
response.headers.associate { it.first to it.second },
response.body.byteStream(),
)
} catch (e: Throwable) {
super.shouldInterceptRequest(view, request)
}
}
}
}

View File

@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
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.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification
@ -51,13 +52,14 @@ import kotlinx.coroutines.flow.onEach
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.core.firebase.FirebaseConfig
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import mihon.telemetry.TelemetryConfig
import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
@ -78,7 +80,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() {
super<Application>.onCreate()
patchInjekt()
FirebaseConfig.init(applicationContext)
TelemetryConfig.init(applicationContext)
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
@ -134,12 +136,20 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
privacyPreferences.analytics()
.changes()
.onEach(FirebaseConfig::setAnalyticsEnabled)
.onEach(TelemetryConfig::setAnalyticsEnabled)
.launchIn(scope)
privacyPreferences.crashlytics()
.changes()
.onEach(FirebaseConfig::setCrashlyticsEnabled)
.onEach(TelemetryConfig::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())
@ -209,17 +219,15 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find {
it.className.equals(
"org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
val isChromiumCall = stackTrace.any { trace ->
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
}
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)
} catch (_: Exception) {
}
return super.getPackageName()
}

View File

@ -8,6 +8,7 @@ import tachiyomi.domain.category.model.Category
class BackupCategory(
@ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0,
@ProtoNumber(3) var id: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
@ProtoNumber(100) var flags: Long = 0,
) {
@ -21,6 +22,7 @@ class BackupCategory(
val backupCategoryMapper = { category: Category ->
BackupCategory(
id = category.id,
name = category.name,
order = category.order,
flags = category.flags,

View File

@ -25,6 +25,7 @@ data class BackupTracking(
@ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0,
) {
@ -48,6 +49,7 @@ data class BackupTracking(
startDate = this@BackupTracking.startedReadingDate,
finishDate = this@BackupTracking.finishedReadingDate,
remoteUrl = this@BackupTracking.trackingUrl,
private = this@BackupTracking.private,
)
}
}
@ -66,6 +68,7 @@ val backupTrackMapper = {
remoteUrl: String,
startDate: Long,
finishDate: Long,
private: Boolean,
->
BackupTracking(
syncId = syncId.toInt(),
@ -80,5 +83,6 @@ val backupTrackMapper = {
startedReadingDate = startDate,
finishedReadingDate = finishDate,
trackingUrl = remoteUrl,
private = private,
)
}

View File

@ -91,7 +91,7 @@ class BackupRestorer(
restoreCategories(backup.backupCategories)
}
if (options.appSettings) {
restoreAppPreferences(backup.backupPreferences)
restoreAppPreferences(backup.backupPreferences, backup.backupCategories.takeIf { options.categories })
}
if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences)
@ -140,9 +140,15 @@ class BackupRestorer(
}
}
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
private fun CoroutineScope.restoreAppPreferences(
preferences: List<BackupPreference>,
categories: List<BackupCategory>?,
) = launch {
ensureActive()
preferenceRestorer.restoreApp(preferences)
preferenceRestorer.restoreApp(
preferences,
categories,
)
restoreProgress += 1
notifier.showRestoreProgress(

View File

@ -404,6 +404,7 @@ class MangaRestorer(
track.remoteUrl,
track.startDate,
track.finishDate,
track.private,
track.id,
)
}

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context
import android.util.Log
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
@ -14,66 +16,122 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.source.sourcePreferences
import tachiyomi.core.common.preference.AndroidPreferenceStore
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.plusAssign
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PreferenceRestorer(
private val context: Context,
private val getCategories: GetCategories = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(),
) {
fun restoreApp(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore)
suspend fun restoreApp(
preferences: List<BackupPreference>,
backupCategories: List<BackupCategory>?,
) {
restorePreferences(
preferences,
preferenceStore,
backupCategories,
)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
fun restoreSource(preferences: List<BackupSourcePreferences>) {
suspend fun restoreSource(preferences: List<BackupSourcePreferences>) {
preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs)
}
}
private fun restorePreferences(
private suspend fun restorePreferences(
toRestore: List<BackupPreference>,
preferenceStore: PreferenceStore,
backupCategories: List<BackupCategory>? = null,
) {
val allCategories = if (backupCategories != null) getCategories.await() else emptyList()
val categoriesByName = allCategories.associateBy { it.name }
val backupCategoriesById = backupCategories?.associateBy { it.id.toString() }.orEmpty()
val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) ->
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
preferenceStore.getInt(key).set(value.value)
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
preferenceStore.getStringSet(key).set(value.value)
try {
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
val newValue = if (key == LibraryPreferences.DEFAULT_CATEGORY_PREF_KEY) {
backupCategoriesById[value.value.toString()]
?.let { categoriesByName[it.name]?.id?.toInt() }
} else {
value.value
}
newValue?.let { preferenceStore.getInt(key).set(it) }
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
val restored = restoreCategoriesPreference(
key,
value.value,
preferenceStore,
backupCategoriesById,
categoriesByName,
)
if (!restored) preferenceStore.getStringSet(key).set(value.value)
}
}
}
} catch (e: Exception) {
Log.e("PreferenceRestorer", "Failed to restore preference <$key>", e)
}
}
}
private fun restoreCategoriesPreference(
key: String,
value: Set<String>,
preferenceStore: PreferenceStore,
backupCategoriesById: Map<String, BackupCategory>,
categoriesByName: Map<String, Category>,
): Boolean {
val categoryPreferences = LibraryPreferences.categoryPreferenceKeys + DownloadPreferences.categoryPreferenceKeys
if (key !in categoryPreferences) return false
val ids = value.mapNotNull {
backupCategoriesById[it]?.name?.let { name ->
categoriesByName[name]?.id?.toString()
}
}
if (ids.isNotEmpty()) {
preferenceStore.getStringSet(key) += ids
}
return true
}
}

View File

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

View File

@ -27,6 +27,9 @@ interface Chapter : SChapter, Serializable {
var version: Long
}
val Chapter.isRecognizedNumber: Boolean
get() = chapter_number >= 0f
fun Chapter.toDomainChapter(): DomainChapter? {
if (id == null || manga_id == null) return null
return DomainChapter(

View File

@ -32,12 +32,15 @@ interface Track : Serializable {
var tracking_url: String
fun copyPersonalFrom(other: Track) {
var private: Boolean
fun copyPersonalFrom(other: Track, copyRemotePrivate: Boolean = true) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
if (copyRemotePrivate) private = other.private
}
companion object {

View File

@ -29,4 +29,6 @@ class TrackImpl : Track {
override var finished_reading_date: Long = 0
override var tracking_url: String = ""
override var private: Boolean = false
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.download
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
@ -454,7 +454,7 @@ private object UniFileAsStringSerializer : KSerializer<UniFile?> {
override fun deserialize(decoder: Decoder): UniFile? {
return if (decoder.decodeNotNullMark()) {
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
UniFile.fromUri(Injekt.get<Application>(), decoder.decodeString().toUri())
} else {
decoder.decodeNull()
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.data.export
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tachiyomi.domain.manga.model.Manga
object LibraryExporter {
data class ExportOptions(
val includeTitle: Boolean,
val includeAuthor: Boolean,
val includeArtist: Boolean,
)
suspend fun exportToCsv(
context: Context,
uri: Uri,
favorites: List<Manga>,
options: ExportOptions,
onExportComplete: () -> Unit,
) {
withContext(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val csvData = generateCsvData(favorites, options)
outputStream.write(csvData.toByteArray())
}
onExportComplete()
}
}
private val escapeRequired = listOf("\r", "\n", "\"", ",")
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
val columnSize = listOf(
options.includeTitle,
options.includeAuthor,
options.includeArtist,
)
.count { it }
val rows = buildList(favorites.size) {
favorites.forEach { manga ->
buildList(columnSize) {
if (options.includeTitle) add(manga.title)
if (options.includeAuthor) add(manga.author)
if (options.includeArtist) add(manga.artist)
}
.let(::add)
}
}
return rows.joinToString("\r\n") { columns ->
columns.joinToString(",") columns@{ column ->
if (column.isNullOrBlank()) return@columns ""
if (escapeRequired.any { column.contains(it) }) {
column.replace("\"", "\"\"").let { "\"$it\"" }
} else {
column
}
}
}
}
}

View File

@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.library
import android.content.Context
import android.content.pm.ServiceInfo
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import androidx.work.BackoffPolicy
import androidx.work.Constraints
@ -92,10 +94,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
if (tags.contains(WORK_NAME_AUTO)) {
val preferences = Injekt.get<LibraryPreferences>()
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.retry()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val preferences = Injekt.get<LibraryPreferences>()
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.retry()
}
}
// 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()
if (interval > 0) {
val restrictions = preferences.autoUpdateDeviceRestrictions().get()
val constraints = Constraints(
requiredNetworkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
},
requiresCharging = DEVICE_CHARGING in restrictions,
requiresBatteryNotLow = true,
)
val networkType = if (DEVICE_NETWORK_NOT_METERED in restrictions) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
}
val networkRequestBuilder = NetworkRequest.Builder()
if (DEVICE_ONLY_ON_WIFI in restrictions) {
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>(
interval.toLong(),

View File

@ -71,6 +71,7 @@ object Notifications {
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
const val ID_APP_UPDATER = 1
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 ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402

View File

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

View File

@ -37,6 +37,8 @@ abstract class BaseTracker(
// Application and remote support for reading dates
override val supportsReadingDates: Boolean = false
override val supportsPrivateTracking: Boolean = false
// TODO: Store all scores as 10 point in the future maybe?
override fun get10PointScore(track: DomainTrack): Double {
return track.score
@ -120,6 +122,11 @@ abstract class BaseTracker(
updateRemote(track)
}
override suspend fun setRemotePrivate(track: Track, private: Boolean) {
track.private = private
updateRemote(track)
}
private suspend fun updateRemote(track: Track): Unit = withIOContext {
try {
update(track)

View File

@ -22,6 +22,8 @@ interface Tracker {
// Application and remote support for reading dates
val supportsReadingDates: Boolean
val supportsPrivateTracking: Boolean
@ColorInt
fun getLogoColor(): Int
@ -82,4 +84,6 @@ interface Tracker {
suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun setRemotePrivate(track: Track, private: Boolean)
}

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
@ -43,6 +42,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val scorePreference = trackPreferences.anilistScoreType()
init {
@ -183,7 +184,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) {

View File

@ -42,8 +42,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
| id
| status
|}
@ -56,6 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt())
put("status", track.toApiStatus())
put("private", track.private)
}
}
with(json) {
@ -79,11 +80,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return withIOContext {
val query = """
|mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) {
|id
@ -102,6 +103,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date))
put("private", track.private)
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
@ -138,6 +140,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|id
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|title {
|userPreferred
|}
@ -190,6 +205,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status
|scoreRaw: score(format: POINT_100)
|progress
|private
|startedAt {
|year
|month
@ -217,6 +233,19 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|month
|day
|}
|staff {
|edges {
|role
|id
|node {
|name {
|full
|userPreferred
|native
|}
|}
|}
|}
|}
|}
|}

View File

@ -19,6 +19,7 @@ data class ALManga(
val startDateFuzzy: Long,
val totalChapters: Long,
val averageScore: Int,
val staff: ALStaff,
) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
remote_id = remoteId
@ -38,6 +39,11 @@ data class ALManga(
""
}
}
staff.edges.forEach {
val name = it.node.name() ?: return@forEach
if ("Story" in it.role) authors += name
if ("Art" in it.role) artists += name
}
}
}
@ -49,6 +55,7 @@ data class ALUserManga(
val startDateFuzzy: Long,
val completedDateFuzzy: Long,
val manga: ALManga,
val private: Boolean,
) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remoteId
@ -60,6 +67,7 @@ data class ALUserManga(
last_chapter_read = chaptersRead.toDouble()
library_id = libraryId
total_chapters = manga.totalChapters
private = this@ALUserManga.private
}
private fun toTrackStatus() = when (listStatus) {

View File

@ -13,6 +13,7 @@ data class ALSearchItem(
val startDate: ALFuzzyDate,
val chapters: Long?,
val averageScore: Int?,
val staff: ALStaff,
) {
fun toALManga(): ALManga = ALManga(
remoteId = id,
@ -24,6 +25,7 @@ data class ALSearchItem(
startDateFuzzy = startDate.toEpochMilli(),
totalChapters = chapters ?: 0,
averageScore = averageScore ?: -1,
staff = staff,
)
}
@ -36,3 +38,31 @@ data class ALItemTitle(
data class ItemCover(
val large: String,
)
@Serializable
data class ALStaff(
val edges: List<ALEdge>,
)
@Serializable
data class ALEdge(
val role: String,
val id: Int,
val node: ALStaffNode,
)
@Serializable
data class ALStaffNode(
val name: ALStaffName,
)
@Serializable
data class ALStaffName(
val userPreferred: String?,
val native: String?,
val full: String?,
) {
operator fun invoke(): String? {
return userPreferred ?: full ?: native
}
}

View File

@ -28,6 +28,7 @@ data class ALUserListItem(
val startedAt: ALFuzzyDate,
val completedAt: ALFuzzyDate,
val media: ALSearchItem,
val private: Boolean,
) {
fun toALUserManga(): ALUserManga {
return ALUserManga(
@ -38,6 +39,7 @@ data class ALUserListItem(
startDateFuzzy = startedAt.toEpochMilli(),
completedDateFuzzy = completedAt.toEpochMilli(),
manga = media.toALManga(),
private = private,
)
}
}

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
@ -23,6 +22,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
private val api by lazy { BangumiApi(id, client, interceptor) }
override val supportsPrivateTracking: Boolean = true
override fun getScoreList(): ImmutableList<String> = SCORE_LIST
override fun displayScore(track: DomainTrack): String {
@ -48,26 +49,23 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val statusTrack = api.statusLibManga(track)
val remoteTrack = api.findLibManga(track)
return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
val statusTrack = api.statusLibManga(track, getUsername())
return if (statusTrack != null) {
track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
track.library_id = statusTrack.library_id
track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = statusTrack.total_chapters
if (track.status != COMPLETED) {
track.status = if (hasReadChapters) READING else statusTrack.status
}
track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track)
update(track)
} else {
// Set default fields if it's not found in the list
track.status = if (hasReadChapters) READING else PLAN_TO_READ
track.score = 0.0
add(track)
update(track)
}
}
@ -76,11 +74,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
}
override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track) ?: throw Exception("Could not find manga")
val remoteStatusTrack = api.statusLibManga(track, getUsername()) ?: throw Exception("Could not find manga")
track.copyPersonalFrom(remoteStatusTrack)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
}
return track
}
@ -113,8 +108,12 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
try {
val oauth = api.accessToken(code)
interceptor.newAuth(oauth)
saveCredentials(oauth.userId.toString(), oauth.accessToken)
} catch (e: Throwable) {
// Users can set a 'username' (not nickname) once which effectively
// replaces the stringified ID in certain queries.
// If no username is set, the API returns the user ID as a strings
var username = api.getUsername()
saveCredentials(username, oauth.accessToken)
} catch (_: Throwable) {
logout()
}
}
@ -126,7 +125,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
fun restoreToken(): BGMOAuth? {
return try {
json.decodeFromString<BGMOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
@ -138,11 +137,11 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
}
companion object {
const val READING = 3L
const val PLAN_TO_READ = 1L
const val COMPLETED = 2L
const val READING = 3L
const val ON_HOLD = 4L
const val DROPPED = 5L
const val PLAN_TO_READ = 1L
private val SCORE_LIST = IntRange(0, 10)
.map(Int::toString)

View File

@ -5,22 +5,28 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchItem
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.common.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class BangumiApi(
private val trackId: Long,
@ -34,11 +40,17 @@ class BangumiApi(
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val body = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toApiStatus())
.build()
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = body))
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
val body = buildJsonObject {
put("type", track.toApiStatus())
put("rate", track.score.toInt().coerceIn(0, 10))
put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
}
.toString()
.toRequestBody()
// Returns with 202 Accepted on success with no body
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
.awaitSuccess()
track
}
@ -46,106 +58,110 @@ class BangumiApi(
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
// read status update
val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toApiStatus())
.build()
authClient.newCall(POST("$API_URL/collection/${track.remote_id}/update", body = sbody))
.awaitSuccess()
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
val body = buildJsonObject {
put("type", track.toApiStatus())
put("rate", track.score.toInt().coerceIn(0, 10))
put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
}
.toString()
.toRequestBody()
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toInt().toString())
val request = Request.Builder()
.url(url)
.patch(body)
.headers(headersOf("Content-Type", APP_JSON))
.build()
authClient.newCall(
POST("$API_URL/subject/${track.remote_id}/update/watched_eps", body = body),
).awaitSuccess()
// Returns with 204 No Content
authClient.newCall(request)
.awaitSuccess()
track
}
}
suspend fun search(search: String): List<TrackSearch> {
// This API is marked as experimental in the documentation
// but that has been the case since 2022 with few significant
// changes to the schema for this endpoint since
// "实验性 API 本 schema 和实际的 API 行为都可能随时发生改动"
return withIOContext {
val url = "$API_URL/search/subject/${URLEncoder.encode(search, StandardCharsets.UTF_8.name())}"
.toUri()
.buildUpon()
.appendQueryParameter("max_results", "20")
.build()
val url = "$API_URL/v0/search/subjects?limit=20"
val body = buildJsonObject {
put("keyword", search)
put("sort", "match")
putJsonObject("filter") {
putJsonArray("type") {
add(1) // "Book" (书籍) type
}
}
}
.toString()
.toRequestBody()
with(json) {
authClient.newCall(GET(url.toString()))
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
.awaitSuccess()
.parseAs<BGMSearchResult>()
.let { result ->
if (result.code == 404) emptyList<TrackSearch>()
result.list
?.filter { it.type == 1 }
?.map { it.toTrackSearch(trackId) }
.orEmpty()
}
.data
.map { it.toTrackSearch(trackId) }
}
}
}
suspend fun findLibManga(track: Track): Track? {
suspend fun statusLibManga(track: Track, username: String): Track? {
return withIOContext {
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
with(json) {
authClient.newCall(GET("$API_URL/subject/${track.remote_id}"))
.awaitSuccess()
.parseAs<BGMSearchItem>()
.toTrackSearch(trackId)
}
}
}
suspend fun statusLibManga(track: Track): Track? {
return withIOContext {
val urlUserRead = "$API_URL/collection/${track.remote_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build()
// TODO: get user readed chapter here
with(json) {
authClient.newCall(requestUserRead)
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
if (it.code == 400) return@let null
track.status = it.status?.id!!
track.last_chapter_read = it.epStatus!!.toDouble()
track.score = it.rating!!
track
try {
authClient.newCall(GET(url, cache = CacheControl.FORCE_NETWORK))
.awaitSuccess()
.parseAs<BGMCollectionResponse>()
.let {
track.status = it.getStatus()
track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
track.score = it.rate?.toDouble() ?: 0.0
track.total_chapters = it.subject?.eps?.toLong() ?: 0L
track
}
} catch (e: HttpException) {
if (e.code == 404) { // "subject is not collected by user"
null
} else {
throw e
}
}
}
}
}
suspend fun accessToken(code: String): BGMOAuth {
return withIOContext {
val body = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET)
.add("code", code)
.add("redirect_uri", REDIRECT_URL)
.build()
with(json) {
client.newCall(accessTokenRequest(code))
client.newCall(POST(OAUTH_URL, body = body))
.awaitSuccess()
.parseAs()
.parseAs<BGMOAuth>()
}
}
}
private fun accessTokenRequest(code: String) = POST(
OAUTH_URL,
body = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", CLIENT_ID)
.add("client_secret", CLIENT_SECRET)
.add("code", code)
.add("redirect_uri", REDIRECT_URL)
.build(),
)
suspend fun getUsername(): String {
return withIOContext {
with(json) {
authClient.newCall(GET("$API_URL/v0/me"))
.awaitSuccess()
.parseAs<BGMUser>()
.username
}
}
}
companion object {
private const val CLIENT_ID = "bgm291665acbd06a4c28"
@ -157,6 +173,8 @@ class BangumiApi(
private const val REDIRECT_URL = "mihon://bangumi-auth"
private const val APP_JSON = "application/json"
fun authUrl(): Uri =
LOGIN_URL.toUri().buildUpon()
.appendQueryParameter("client_id", CLIENT_ID)

View File

@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
@ -21,12 +20,13 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val currAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
var currAuth: BGMOAuth = oauth ?: throw Exception("Not authenticated with Bangumi")
if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
if (response.isSuccessful) {
newAuth(json.decodeFromString<BGMOAuth>(response.body.string()))
currAuth = json.decodeFromString<BGMOAuth>(response.body.string())
newAuth(currAuth)
} else {
response.close()
}
@ -38,14 +38,7 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
"antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
)
.apply {
if (originalRequest.method == "GET") {
val newUrl = originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.accessToken)
.build()
url(newUrl)
} else {
post(addToken(currAuth.accessToken, originalRequest.body as FormBody))
}
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
}
.build()
.let(chain::proceed)
@ -67,13 +60,4 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
bangumi.saveToken(oauth)
}
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0..<oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
}
}

View File

@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.bangumi
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toApiStatus() = when (status) {
Bangumi.READING -> "do"
Bangumi.COMPLETED -> "collect"
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLAN_TO_READ -> "wish"
Bangumi.PLAN_TO_READ -> 1
Bangumi.COMPLETED -> 2
Bangumi.READING -> 3
Bangumi.ON_HOLD -> 4
Bangumi.DROPPED -> 5
else -> throw NotImplementedError("Unknown status: $status")
}

View File

@ -1,28 +1,34 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
// Incomplete DTO with only our needed attributes
data class BGMCollectionResponse(
val code: Int?,
val `private`: Int? = 0,
val comment: String? = "",
val rate: Int?,
val type: Int?,
@SerialName("ep_status")
val epStatus: Int? = 0,
@SerialName("lasttouch")
val lastTouch: Int? = 0,
val rating: Double? = 0.0,
val status: Status? = Status(),
val tag: List<String?>? = emptyList(),
val user: User? = User(),
@SerialName("vol_status")
val volStatus: Int? = 0,
)
val private: Boolean = false,
val subject: BGMSlimSubject? = null,
) {
fun getStatus(): Long = when (type) {
1 -> Bangumi.PLAN_TO_READ
2 -> Bangumi.COMPLETED
3 -> Bangumi.READING
4 -> Bangumi.ON_HOLD
5 -> Bangumi.DROPPED
else -> throw NotImplementedError("Unknown status: $type")
}
}
@Serializable
data class Status(
val id: Long? = 0,
val name: String? = "",
val type: String? = "",
// Incomplete DTO with only our needed attributes
data class BGMSlimSubject(
val volumes: Int?,
val eps: Int?,
)

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -10,6 +11,7 @@ data class BGMOAuth(
@SerialName("token_type")
val tokenType: String,
@SerialName("created_at")
@EncodeDefault
val createdAt: Long = System.currentTimeMillis() / 1000,
@SerialName("expires_in")
val expiresIn: Long,

View File

@ -6,40 +6,50 @@ import kotlinx.serialization.Serializable
@Serializable
data class BGMSearchResult(
val list: List<BGMSearchItem>?,
val code: Int?,
val total: Int,
val limit: Int,
val offset: Int,
val data: List<BGMSubject> = emptyList(),
)
@Serializable
data class BGMSearchItem(
// Incomplete DTO with only our needed attributes
data class BGMSubject(
val id: Long,
@SerialName("name_cn")
val nameCn: String,
val name: String,
val type: Int,
val images: BGMSearchItemCovers?,
@SerialName("eps_count")
val epsCount: Long?,
val rating: BGMSearchItemRating?,
val url: String,
val summary: String?,
val date: String?, // YYYY-MM-DD
val images: BGMSubjectImages?,
val volumes: Long = 0,
val eps: Long = 0,
val rating: BGMSubjectRating?,
) {
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
remote_id = this@BGMSearchItem.id
title = nameCn
cover_url = images?.common ?: ""
summary = this@BGMSearchItem.name
remote_id = this@BGMSubject.id
title = nameCn.ifBlank { name }
cover_url = images?.common.orEmpty()
summary = if (nameCn.isNotBlank()) {
"作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
} else {
this@BGMSubject.summary?.trim().orEmpty()
}
score = rating?.score ?: -1.0
tracking_url = url
total_chapters = epsCount ?: 0
tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
total_chapters = eps
start_date = date ?: ""
}
}
@Serializable
data class BGMSearchItemCovers(
// Incomplete DTO with only our needed attributes
data class BGMSubjectImages(
val common: String?,
)
@Serializable
data class BGMSearchItemRating(
// Incomplete DTO with only our needed attributes
data class BGMSubjectRating(
val score: Double?,
)

View File

@ -1,23 +1,9 @@
package eu.kanade.tachiyomi.data.track.bangumi.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = "",
)
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,
val nickname: String? = "",
val sign: String? = "",
val url: String? = "",
@SerialName("usergroup")
val userGroup: Int? = 0,
val username: String? = "",
// Incomplete DTO with only our needed attributes
data class BGMUser(
val username: String,
)

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
@ -29,6 +28,8 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val json: Json by injectLazy()
private val interceptor by lazy { KitsuInterceptor(this) }
@ -101,7 +102,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.remote_id = remoteTrack.remote_id
if (track.status != COMPLETED) {
@ -150,7 +151,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
fun restoreToken(): KitsuOAuth? {
return try {
json.decodeFromString<KitsuOAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}

View File

@ -46,6 +46,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
putJsonObject("attributes") {
put("status", track.toApiStatus())
put("progress", track.last_chapter_read.toInt())
put("private", track.private)
}
putJsonObject("relationships") {
putJsonObject("user") {
@ -94,6 +95,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
put("ratingTwenty", track.toApiScore())
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
put("private", track.private)
}
}
}

View File

@ -42,6 +42,7 @@ data class KitsuListSearchResult(
}
score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0
last_chapter_read = userDataAttrs.progress.toDouble()
private = userDataAttrs.private
}
}
}
@ -59,6 +60,7 @@ data class KitsuListSearchItemDataAttributes(
val finishedAt: String?,
val ratingTwenty: Int?,
val progress: Int,
val private: Boolean,
)
@Serializable

View File

@ -30,8 +30,14 @@ class TrackSearch : Track {
override var finished_reading_date: Long = 0
override var private: Boolean = false
override lateinit var tracking_url: String
var authors: List<String> = emptyList()
var artists: List<String> = emptyList()
var cover_url: String = ""
var summary: String = ""

View File

@ -111,7 +111,7 @@ class MyAnimeListApi(
summary = it.synopsis
total_chapters = it.numChapters
score = it.mean
cover_url = it.covers.large
cover_url = it.covers?.large.orEmpty()
tracking_url = "https://myanimelist.net/manga/$remote_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist.dto
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -14,10 +15,11 @@ data class MALOAuth(
@SerialName("expires_in")
val expiresIn: Long,
@SerialName("created_at")
val createdAt: Long = System.currentTimeMillis(),
@EncodeDefault
val createdAt: Long = System.currentTimeMillis() / 1000,
) {
// Assumes expired a minute earlier
private val adjustedExpiresIn: Long = (expiresIn - 60) * 1000
private val adjustedExpiresIn: Long = (expiresIn - 60)
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis()
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() / 1000
}

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.release.interactor.GetApplicationRelease
import uy.kohesive.injekt.injectLazy
@ -20,8 +20,7 @@ class AppUpdateChecker {
return withIOContext {
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
BuildConfig.PREVIEW,
context.isInstalledFromFDroid(),
isPreviewBuildType,
BuildConfig.COMMIT_COUNT.toInt(),
BuildConfig.VERSION_NAME,
GITHUB_REPO,
@ -31,9 +30,6 @@ class AppUpdateChecker {
when (result) {
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(
context,
).promptFdroidUpdate()
else -> {}
}
@ -43,7 +39,7 @@ class AppUpdateChecker {
}
val GITHUB_REPO: String by lazy {
if (BuildConfig.PREVIEW) {
if (isPreviewBuildType) {
"mihonapp/mihon-preview"
} else {
"mihonapp/mihon"
@ -51,7 +47,7 @@ val GITHUB_REPO: String by lazy {
}
val RELEASE_TAG: String by lazy {
if (BuildConfig.PREVIEW) {
if (isPreviewBuildType) {
"r${BuildConfig.COMMIT_COUNT}"
} else {
"v${BuildConfig.VERSION_NAME}"

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