Compare commits

..
132 Commits
Author SHA1 Message Date
AntsyLich f9f938f5af Release v0.19.4 2026-02-26 18:27:20 +06:00
AntsyLich 1d7c838ae6 Revert "Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955)"
This reverts commit  22d5c9d9f4
2026-02-26 18:25:56 +06:00
AntsyLich d219c5e3bb Revert "Fix cache invalidation isn't done at startup (#2970)"
This reverts commit e6cc38de06.
2026-02-26 18:25:56 +06:00
cc3f640faa Translations update from Hosted Weblate (#2997)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon

Co-authored-by: Filip Jaruška <filip.jaruska@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
2026-02-26 18:24:45 +06:00
AntsyLichandp 84265febf3 Fix extension install/update stuck at pending (#3000)
Co-authored-by: p
2026-02-26 01:58:38 +00:00
Mend Renovate 171a06a8ba Update dependency io.kotest:kotest-assertions-core to v6.1.4 (#2998) 2026-02-26 07:51:00 +06:00
565379779f Translations update from Hosted Weblate (#2980)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
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/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
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/
Translation: Mihon/Mihon

Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Jakub Szafranek13 Fabijan <jakubfabijan@tuta.io>
Co-authored-by: KraXen <dpelech1@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
2026-02-25 19:13:17 +06:00
AntsyLichandjeremiejig 0394714274 Run automatic library updates even when connected to a VPN (#2996)
Co-authored-by: jeremiejig <3978761+jeremiejig@users.noreply.github.com>
2026-02-25 13:05:30 +00:00
Cuong-TranandAntsyLich 75b445fa8f Going back now first clears search query on browse extension tab (#2906)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-02-25 12:41:45 +00:00
AntsyLich 0cc724108b Add all pages of adjacent chapters in the UI instead of only the first or last three (#2995) 2026-02-25 12:28:53 +00:00
Mend Renovate 1e6c35d887 Update gradle/actions action to v5.0.2 (#2990) 2026-02-25 18:22:10 +06:00
Mend Renovate 5b85084d0a Update dependency io.coil-kt.coil3:coil-bom to v3.4.0 (#2992) 2026-02-25 18:21:55 +06:00
AntsyLich 8b2d35f306 Fix migration dialog not showing for consecutive prompts from the same screen (#2994) 2026-02-25 11:15:13 +00:00
AntsyLich 47816d4b21 Fix migration's selected sources order not preserved (#2993) 2026-02-25 17:04:24 +06:00
NarwhalHornsandAntsyLich 89bbdb17fb Utilize tracker for library duplicate detection (#2978)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-02-21 10:26:36 +00:00
NarwhalHorns 3c6f0f1697 Add option for bookmarked chapters to download dropdown (#2891) 2026-02-21 16:14:55 +06:00
ab214526c6 Optimize tracked library filter (#2977)
Co-authored-by: NarwhalHorns <onefailedgamer@gamil.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-02-20 16:22:46 +00:00
Luca Auer e6cc38de06 Fix cache invalidation isn't done at startup (#2970) 2026-02-20 21:09:00 +06:00
+12 7c7d35e2c6 Translations update from Hosted Weblate (#2843)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/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/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
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

Co-authored-by: Ai Ai Ecchi <aiaiecchi9og@gmail.com>
Co-authored-by: Alexandre Dhooge <alexandre.dhooge@zaclys.net>
Co-authored-by: Cuong Tran <cuongtran.tm@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Hualiang <642615676@qq.com>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: Jakub Szafranek13 Fabijan <jakubfabijan@tuta.io>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Throw Away <throwawayacc4gulshan@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: akir45 <akkn0708@gmail.com>
Co-authored-by: clukki <nguyenhuuminh16911@gmail.com>
Co-authored-by: momoehab <momoehab@gmail.com>
Co-authored-by: yumekon <konrad.nowicki91@gmail.com>
Co-authored-by: ɴᴇᴋᴏ <s99095lkjjim@gmail.com>
Co-authored-by: 안세훈 <on9686@gmail.com>
2026-02-20 20:35:25 +06:00
Cuong-Tran 9867c160f7 Remove redundant userSelected from selection methods (#2976) 2026-02-20 20:22:05 +06:00
Cuong-Tran 20c899f2cd Don't wrap an intent-chooser inside another intent-chooser (#2921) 2026-02-20 20:20:13 +06:00
Mend Renovate 3f64529412 Update actions/dependency-review-action action to v4.8.3 (#2975) 2026-02-20 20:18:28 +06:00
Mend Renovate 27749fc583 Update moko to v0.26.0 (#2967) 2026-02-15 15:57:44 +00:00
Mend Renovate 2248bdac54 Update dependency com.google.firebase:firebase-bom to v34.9.0 (#2964) 2026-02-15 21:48:42 +06:00
Mend Renovate 42c7329a9e Update dependency org.junit.jupiter:junit-jupiter to v6.0.3 (#2963) 2026-02-15 21:48:08 +06:00
Luca Auer 22d5c9d9f4 Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955) 2026-02-15 21:47:31 +06:00
Mend Renovate 5be5a4e819 Update dependency androidx.compose:compose-bom to v2026.02.00 (#2962) 2026-02-15 20:37:53 +06:00
Mend Renovate b924d582f4 Update dependency androidx.activity:activity-compose to v1.12.4 (#2959) 2026-02-15 20:36:33 +06:00
Mend Renovate 6f601a18c5 Update kotlin monorepo to v2.3.10 (#2960) 2026-02-15 20:36:17 +06:00
Mend Renovate 1ff9a5625c Update paging.version to v3.4.1 (#2961) 2026-02-15 20:36:02 +06:00
Luca Auer cae9fbf321 Add missing indexes to improve database query performance (#2950) 2026-02-15 00:43:31 +06:00
Mend Renovate d17976c910 Update dependency io.kotest:kotest-assertions-core to v6.1.3 (#2939) 2026-02-09 06:32:07 +06:00
Mend Renovate b609166702 Update dependency com.materialkolor:material-kolor to v5.0.0-alpha06 (#2938) 2026-02-09 06:31:53 +06:00
MajorTanya cf93afab45 Add src:local search alias for Local Source (#2928) 2026-02-06 01:15:34 +06:00
MajorTanya b543bc089a Clean up some build warnings (#2929)
* Replace deprecated rememberPlainTooltipPositionProvider

* Remove superfluous when branch

This when is marked as exhaustive.

* Replace deprecated LibrariesContainer call

AboutLibraries now wants us to produce the libraries ourselves.

* Replace deprecated ClipboardManager with Clipboard

Clipboard uses suspend functions, hence the coroutine scope addition.

* Use multi-dollar strs to simplify GraphQL queries

These have been available since Kotlin 2.1.

* Remove various redundant casts & conversions

- WebViewScreenContent: loadingState is in the LoadingState.Loading
  branch, no need to cast at all
- Bangumi: username is not modified, make val
- Kavita: token is already a String
- PagerViewerAdapter: insertPageLastPage is already null-checked
- PagerViewerAdapter: use reified filterIsInstance
- ReaderViewModel: chapter IDs are already Longs
- CloudflareInterceptor: webview is smart-cast to non-null here

* Replace deprecated MenuAnchorType

Literally just a typealias for ExposedDropdownMenuAnchorType anyway.

* OptimizeNonSkippingGroups is enabled by default

* Suppress shadowing warning

This is explicitly intentional according to the KDocs.

* Migrate Context Receivers to Context Parameters

Requires changing the compiler arg, but that is part of the migration:

https://blog.jetbrains.com/kotlin/2025/04/update-on-context-parameters

Apparently, the only visible change is that names are required now.
"_" can be used for anonymous context parameters.

* Fix expression bodies with explicit return

Naming conflict resolved by aliasing.

From 2.4/2.5 onward, these will only be allowed with explicit return
types, or have to be turned into a block body. I opted for the latter
since the function is reasonably dense already.

see: https://youtrack.jetbrains.com/issue/KTLC-288

* Suppress deprecation of non-AutoMirrored icons

We use these arrows for navigation in the Upcoming screen.
I strongly doubt the AutoMirrored versions would make sense for our
use-case.

* Explicitly opt-in to new annotation default rules

affects the following annotated value-parameters:
- Preference.SliderPreference.steps (`@IntRange`)
- ReaderViewModel.State.brightnessOverlayValue (`@IntRange`)
- ReadingMode.iconRes (`@DrawableRes`)
- MigrationListScreenModel.Dialog.Progress.progress (`@FloatRange`)

see: https://youtrack.jetbrains.com/issue/KT-73255
see: https://github.com/Kotlin/KEEP/blob/change-defaulting-rule/proposals/annotation-target-in-properties.md

Warning message was the following:

    This annotation is currently applied to the value parameter only, but in the future it will also be applied to field.
    - To opt in to applying to both value parameter and field, add '-Xannotation-default-target=param-property' to your compiler arguments.
    - To keep applying to the value parameter only, use the '@param:' annotation target.
2026-02-05 11:29:21 +06:00
MajorTanya 4e4bdffdf3 Add "src:" prefix to search by source ID (#2927)
Allows users to search for the exact source ID in their library.

Similar to the Browse > Migrate screen, but filtered in the Library,
where users can take all the usual actions.

Could be used in the future to change the search behaviour of tapping
the source name in the title info view to search by the ID with this
prefix.
2026-02-03 15:54:22 +06:00
Mend Renovate 100cea0757 Update dependency androidx.compose:compose-bom to v2026.01.01 (#2913) 2026-02-02 12:33:28 +06:00
Mend Renovate 86344e20d3 Update gradle/actions action to v5.0.1 (#2912) 2026-02-02 12:31:47 +06:00
Mend Renovate e092b4208a Update dependency androidx.work:work-runtime to v2.11.1 (#2914) 2026-02-02 12:31:29 +06:00
Mend Renovate 1bf4eff931 Update paging.version to v3.4.0 (#2916) 2026-02-02 12:28:25 +06:00
Mend Renovate b70edfac58 Update dependency androidx.activity:activity-compose to v1.12.3 (#2917) 2026-02-02 12:27:47 +06:00
Mend Renovate 9c0eebb55c Update markdown to v0.39.2 (#2923) 2026-02-02 12:27:27 +06:00
Mend Renovate f6f1d13add Update dependency io.kotest:kotest-assertions-core to v6.1.2 (#2908) 2026-01-28 07:30:38 +06:00
Mend Renovate 31a9ff261e Update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.2.1 (#2909) 2026-01-28 07:30:23 +06:00
Mend Renovate c0a1203541 Update dependency io.mockk:mockk to v1.14.9 (#2904) 2026-01-27 01:24:06 +06:00
Cuong-Tran 82ffc8efa6 Fix Add Repo input not taking up the full dialog width (#2816) 2026-01-24 19:50:15 +06:00
Mend Renovate d6e17b04c3 Update serialization.version to v1.10.0 (#2879) 2026-01-24 19:48:43 +06:00
Mend Renovate e6c6700351 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.2.0 (#2892) 2026-01-24 19:42:37 +06:00
Mend Renovate ede2f56bc6 Update dependency io.kotest:kotest-assertions-core to v6.1.1 (#2893) 2026-01-24 19:42:16 +06:00
Mend Renovate 2037ec4500 Update Gradle to v8.14.4 (#2894) 2026-01-24 19:41:59 +06:00
Cuong-Tran 533a578bdb Fix memoization in manga bottom action menus (#2886) 2026-01-24 19:41:18 +06:00
Mend Renovate fa966fcb97 Update GitHub Actions (#2884) 2026-01-24 19:38:36 +06:00
MajorTanya 05d90ea4d6 Reword download index message (#2874)
I'm tired of people thinking the current wording is an error. Improved wordings welcome, this was just my first decent guess.
2026-01-20 14:58:41 +00:00
Mend Renovate 49c4d08b22 Update dependency io.kotest:kotest-assertions-core to v6.1.0 (#2870) 2026-01-20 20:39:01 +06:00
MajorTanyaandDani bbe9aa8561 Add Filters to Updates screen (#2851)
* Add Filters to Updates screen

Behaves basically like the filters in the library:

- Unread: Show/Don't show unread chapters
- Downloaded: Show/Don't show downloaded chapters
- Started: Show/Don't show chapters that have some progress but aren't
  fully Read
- Bookmarked: Show/Don't show chapters that have been bookmarked

Started behaves differently from its Library counterpart because the
actual manga data is not available at this point in time and I thought
calling getManga for each entry without caching would be a pretty bad
idea.

I have modelled this closely on the filter control flow in the
Library, but I'm sure this can be simplified/adjusted in some way.

* Move most filtering logic to SQL

Unread, Started, and Bookmarked filters are now part of the SQL query.

Download state cannot be filtered in the database so it remains in
Kotlin.

Because the Downloaded filter has to be run in Kotlin, the combine
flow uses the preferences flow twice, once to get the SQL query params
and once for the Kotlin filters (only Downloaded at this time).

* Add "Hide excluded scanlators" to update filters

Based on the work done in #1623 but integrated with the other filters
in this PR. Added the user as a co-author for credit.

Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com>

---------

Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com>
2026-01-17 10:43:40 +00:00
Mend Renovate 13975d6f7e Update dependency com.google.firebase:firebase-bom to v34.8.0 (#2856) 2026-01-16 17:06:02 +06:00
Mend Renovate e659e90c26 Update dependency androidx.compose:compose-bom to v2026 (#2853) 2026-01-15 19:50:16 +06:00
Mend Renovate 99e6fa3c06 Update dependency com.materialkolor:material-kolor to v5.0.0-alpha05 (#2849) 2026-01-14 21:35:51 +06:00
Mend Renovate cf13012629 Update markdown to v0.39.1 (#2850) 2026-01-14 21:35:37 +06:00
AntsyLich 93b355a9ef Update donation link [skip ci] 2026-01-12 22:41:10 +06:00
NGB-Was-Taken a4f5a8184c Enable logcat logging on stable and debug builds without enabling verbose logging (#2836) 2026-01-07 22:22:52 +06:00
NGB-Was-Taken 4ce249c1a0 Fix crash when trying to install/update extensions while shizuku isn't running (#2837) 2026-01-07 22:17:53 +06:00
MajorTanya edcf84d902 Fix nullability of MAL authors breaking search (#2834)
One of these days I'll get through a tracker change without
nullability problems...
2026-01-07 19:30:44 +06:00
Constantin PiberandAntsyLich bd5c4d48f9 Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi (#2675)
Due to a `Float->Double->Float` conversion somewhere inside Mihon, the
tracker sees 2.1 as 2.0999999046325684, which means this filter ignores
the 2.1 chapter (which we just tried to mark as read). This small
epsilon is small enough to never bother any serious usage, but large
enough to ignore any such conversion errors.

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2026-01-07 08:36:37 +00:00
MajorTanya 51b3ab3fd1 Add authors/artists to MAL search results (#2833) 2026-01-07 08:16:56 +00:00
e6f72000ba Translations update from Hosted Weblate (#2806)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Ahmed TOUCHANE <ahmedtouchane0@gmail.com>
Co-authored-by: Anderhale <anderhale@users.noreply.hosted.weblate.org>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
Co-authored-by: Hiroshi <borlonjhayron1119@gmail.com>
Co-authored-by: Luis Antonio <getcyonic+zaorinu@gmail.com>
Co-authored-by: NormalRandomPeople <normal.scribe833@silomails.com>
2026-01-07 14:13:21 +06:00
MajorTanya 9bf2d78a42 Optimise MAL search queries by ~11x (#2832)
Previously, the app made one request for the search, and then fired
off 1 request per search result to obtain additional data, such as
each title's synopsis, etc.

However, MAL's search allows field selection during the initial query,
which will return all the data in that first response, avoiding the
massive bunch of requests (and alleviating some pressure on MAL from
our userbase).

By combining the selected fields into one constant, I was able to also
get rid of the MALUserListSearch entirely because it was redundant.
This allows for a unified MALManga->TrackSearch helper, further
reducing complexity.

I got to my "11x" improvement because on page of search results has 10
elements, and this change turns 11 (1+10 for results) requests into 1.
2026-01-07 13:59:42 +06:00
Mend Renovate 89c4e3bb39 Update dependency org.junit.jupiter:junit-jupiter to v6.0.2 (#2830) 2026-01-06 18:14:34 +00:00
Mend Renovate 47fe792ddc Update dependency org.jsoup:jsoup to v1.22.1 (#2826) 2026-01-07 00:04:38 +06:00
AntsyLichandCuong-Tran 906d6f3cdb Remember descriptionAnnotator across composition
Closes #2510

Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
2025-12-27 01:43:39 +06:00
AntsyLich e059190fab Cleanup extension screen search query predicate 2025-12-27 01:34:00 +06:00
AntsyLich a39b5a56e8 Switch to M3E ExtendedFloatingActionButton 2025-12-27 01:04:49 +06:00
AntsyLich 3e6afee13b Switch to MaterialExpressiveTheme 2025-12-27 00:50:03 +06:00
AntsyLich 9a11ec8ead Use materilalKolor for monet compat color scheme 2025-12-27 00:31:18 +06:00
Constantin PiberandAntsyLich 1263df9d41 Implement automatic removal of downloads on Suwayomi after reading, configurable via extension settings (#2673)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-12-20 12:54:17 +00:00
c96b6ae562 Translations update from Hosted Weblate (#2711)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Anderhale <anderhale@users.noreply.hosted.weblate.org>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Hasanur Rahman Biplob <hrbiplob10@gmail.com>
Co-authored-by: Jakub Szafranek13 Fabijan <jakubfabijan@tuta.io>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
Co-authored-by: ابْنُ السَدِيمِ <amarlubs2@gmail.com>
Co-authored-by: 안세훈 <on9686@gmail.com>
2025-12-20 18:51:10 +06:00
Mend Renovate 23c427cf60 Update dependency androidx.compose:compose-bom to v2025.12.01 (#2651) 2025-12-20 18:34:06 +06:00
Mend Renovate e3260d56f7 Update markdown to v0.39.0 (#2804) 2025-12-20 18:33:50 +06:00
Mend Renovate f37afbcec9 Update aboutlib.version to v13.2.1 (#2803) 2025-12-20 18:33:30 +06:00
MajorTanya 7a1c8a1b61 Fix pre-1970 upload date display in chapter list (#2779)
A user in #2777 was using the ComicInfo.xml Year/Month/Day fields to
indicate date of publication for some American comics, which often
predate the UNIX Epoch of 1970.

They were seeing "N/A" displays because this line of code discarded
date information for any time before Jan 1st, 1970.

The `toRelativeString` extension function used in the other
`relativeDateText` function already accounts for very distant dates
(anything >7 days away turns into full date, not relative, regardless
of setting, though disabling the relative timestamp setting
circumvents this with the same result). Removing this line should not
cause any issues as it is purely a display difference and the use case
of backdating comics to pre-1970 is worth it in my opinion.
2025-12-18 11:26:35 +00:00
Mend Renovate 532b5cf290 Update dependency androidx.activity:activity-compose to v1.12.2 (#2797) 2025-12-18 16:44:50 +06:00
Mend Renovate 3cb1b2e17a Update kotlin monorepo to v2.3.0 (#2794) 2025-12-16 23:10:37 +06:00
Luca AuerandAntsyLich 4c9cfd8da5 Minimize memory usage by reducing in-memory cover cache size (#2266)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-12-16 23:08:24 +06:00
Mend Renovate 2ecb70a590 Update GitHub Actions (#2786) 2025-12-13 14:59:27 +06:00
AntsyLich 2e0786f699 Fix reader not saving read duration when changing chapter (#2784) 2025-12-12 18:49:57 +00:00
Mend Renovate e7e4d9b6b3 Update dependency com.google.firebase:firebase-bom to v34.7.0 (#2782) 2025-12-13 00:22:54 +06:00
AntsyLich 5fe7dd9f06 Use AGP provided NDK and Build Tools version 2025-12-13 00:18:48 +06:00
Mend Renovate 4cb05cc738 Update dependency com.android.tools.build:gradle to v8.13.2 (#2780) 2025-12-11 20:56:36 +06:00
AntsyLich 876c3f951b Update tracker icons (#2773) 2025-12-10 15:19:46 +06:00
Mend Renovate 08a61a42e9 Update dependency androidx.activity:activity-compose to v1.12.1 (#2760) 2025-12-07 12:35:09 +06:00
Mend Renovate cadd36ad9a Update dependency io.mockk:mockk to v1.14.7 (#2771) 2025-12-07 12:34:59 +06:00
Mend Renovate 97327555b3 Update GitHub Actions (#2757) 2025-12-07 12:33:47 +06:00
Mend Renovate 5b88f88198 Update softprops/action-gh-release action to v2.5.0 (#2750) 2025-12-01 23:38:05 +06:00
Mend Renovate 556371e1c8 Update dependency io.kotest:kotest-assertions-core to v6.0.7 (#2749) 2025-12-01 23:37:56 +06:00
AntsyLich c222a28bd1 Cleanup BaseOAuthLoginActivity and TrackLoginActivity (#2748) 2025-11-30 12:17:25 +00:00
AntsyLich 7161bc2e82 Fix mass migration not using the same search queries as individual migration (#2736) 2025-11-23 18:05:30 +00:00
NGB-Was-TakenandAntsyLich c3d858a561 Fix shizuku installer not updating installed extensions (#2697)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-23 08:16:38 +00:00
Mend RenovateandAntsyLich 13552c5ffa Update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0 (#2708)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-11-23 07:55:54 +00:00
Mend Renovate e6ca458e1f Update dependency androidx.activity:activity-compose to v1.12.0 (#2725) 2025-11-23 13:31:50 +06:00
Mend Renovate 05c7df2ed3 Update moko to v0.25.2 (#2723) 2025-11-23 13:29:50 +06:00
Mend Renovate 4a3339a21f Update sqlite to v2.6.2 (#2724) 2025-11-23 13:29:28 +06:00
Mend Renovate 6f497ed031 Update lifecycle.version to v2.10.0 (#2726) 2025-11-23 13:29:06 +06:00
Mend Renovate 8b862c2e46 Update actions/checkout action to v6 (#2729) 2025-11-23 13:28:20 +06:00
Mend Renovate fc2c8c06a9 Update actions/checkout action to v5.0.1 (#2715) 2025-11-19 14:10:08 +06:00
Mend Renovate c17fc6792a Update dependency com.squareup.okio:okio to v3.16.4 (#2716) 2025-11-19 14:09:51 +06:00
Mend Renovate 9e27ae3d2b Update dependency io.kotest:kotest-assertions-core to v6.0.5 (#2717) 2025-11-19 14:09:34 +06:00
Mend Renovate 8b5dd39f1c Update okhttp monorepo to v5.3.2 (#2720) 2025-11-19 14:08:15 +06:00
Mend Renovate 8d81c94679 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v8.1.0 (#2721) 2025-11-19 14:07:57 +06:00
Mend Renovate e5a693f224 Update dependency com.google.firebase:firebase-bom to v34.6.0 (#2707) 2025-11-17 13:58:37 +06:00
Mend Renovate 7947e1bd6a Update okhttp monorepo to v5.3.1 (#2712) 2025-11-17 13:53:23 +06:00
Mend Renovate cab729c939 Update dependency com.squareup.okio:okio to v3.16.3 (#2709) 2025-11-15 13:27:50 +06:00
Mend Renovate f6a95a7416 Update dependency app.cash.sqldelight:sqlite-3-38-dialect to v2.2.1 (#2703) 2025-11-14 07:17:55 +00:00
32db0a5f4c Translations update from Hosted Weblate (#2687)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/bn/
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/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Anderhale <anderhale@users.noreply.hosted.weblate.org>
Co-authored-by: Anderhale <safwanistaken@gmail.com>
Co-authored-by: Ardiansyahset <ardiantozep@users.noreply.hosted.weblate.org>
Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Co-authored-by: MajorTanya <github@majortanya.eu>
Co-authored-by: NGB-Was-Taken <76197326+NGB-Was-Taken@users.noreply.github.com>
Co-authored-by: amigo browser <juniperforest1@proton.me>
2025-11-14 13:10:34 +06:00
NGB-Was-Taken 2ec67ac0c1 Stop tap zones from triggering when scrolling is stopped by tapping (#2680) 2025-11-14 13:10:19 +06:00
Mend Renovate c2bc95191b Update actions/dependency-review-action action to v4.8.2 (#2686)
Update GitHub Actions
2025-11-14 13:09:25 +06:00
Mend Renovate b1b79a63f0 Update sqldelight to v2.2.1 (#2704) 2025-11-14 13:07:51 +06:00
Mend Renovate ec99ab3aef Update dependency com.android.tools.build:gradle to v8.13.1 (#2685) 2025-11-11 14:29:40 +06:00
Mend Renovate 69e5a1aea7 Update softprops/action-gh-release action to v2.4.2 (#2683) 2025-11-11 14:28:42 +06:00
+2 84d620978b Translations update from Hosted Weblate (#2676)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
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/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/
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/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/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/he/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
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/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/
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/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/
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/uk/
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: FateXBlood <fatexblood@gmail.com>
Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: MajorTanya <github@majortanya.eu>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: NGB-Was-Taken <76197326+NGB-Was-Taken@users.noreply.github.com>
Co-authored-by: NGB-Was-Taken <myalternate34@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
2025-11-11 14:27:44 +06:00
AntsyLich 412815af06 Revert "Fix reader tap zones triggering after scrolling was stopped by the user" (#2670) 2025-11-07 13:07:14 +00:00
f7fb68692a Translations update from Hosted Weblate (#2656)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
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: Doministo <doministo@seznam.cz>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: TheKingTermux <50316075+TheKingTermux@users.noreply.github.com>
Co-authored-by: ZerOriSama <godarms2010@live.com>
Co-authored-by: amigo browser <juniperforest1@proton.me>
2025-11-07 19:03:52 +06:00
AntsyLich aa300cb53e Fix extra padding appearing in reader after user interactions (#2669) 2025-11-07 10:00:59 +00:00
Trevor Paley 855eea2ada Improve WebView multi-window UX (#2662)
- Navigation history for lower windows is preserved when a popup is opened
- Back gesture will close a popup window rather than the entire WebView activity when there is no previous page
- The leftmost close button closes the entire activity as before
- When a popup window is shown, a new button appears to close just that window
2025-11-07 15:26:04 +06:00
Mend Renovate f4703ed83a Update dependency androidx.core:core-splashscreen to v1.2.0 (#2661) 2025-11-07 15:22:41 +06:00
NGB-Was-Taken 506d51a007 Fix flaky migration tests (#2663) 2025-11-07 15:21:17 +06:00
NGB-Was-Taken 9f9155121c Upload test report as artifact on failure (#2664) 2025-11-07 15:20:32 +06:00
AntsyLich 282110ef21 Release v0.19.3 2025-11-04 13:05:40 +06:00
AntsyLich ace387f8bf Revert "Update dependency androidx.compose:compose-bom to v2025.10.01 (#2522)"
This reverts commit e8bdf58530.
2025-11-04 13:05:23 +06:00
5e428071c9 Translations update from Hosted Weblate (#2646)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nl/
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/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Siebrenvde <siebren@siebrenvde.dev>
2025-11-04 13:04:46 +06:00
AntsyLich 0acd80dd95 Fix long strip reader not scrolling on consecutive taps (#2650) 2025-11-04 06:46:48 +00:00
bapeey bdb0ce4779 Fix WebView crash introduced in v0.19.2 (#2649) 2025-11-04 11:43:40 +06:00
Mend Renovate e8bdf58530 Update dependency androidx.compose:compose-bom to v2025.10.01 (#2522) 2025-11-03 10:52:45 +06:00
205 changed files with 2752 additions and 1426 deletions
+1
View File
@@ -29,3 +29,4 @@ ktlint_standard_function-expression-body = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_type-argument-comment = disabled
ktlint_standard_type-parameter-comment = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
+1
View File
@@ -0,0 +1 @@
patreon: mihon
+1 -1
View File
@@ -30,7 +30,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.19.4](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true
+2 -2
View File
@@ -52,7 +52,7 @@ body:
label: Mihon version
description: You can find your Mihon version in **More → About**.
placeholder: |
Example: "0.19.2"
Example: "0.19.4"
validations:
required: true
@@ -95,7 +95,7 @@ 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.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
- label: I have updated the app to version **[0.19.4](https://github.com/mihonapp/mihon/releases/latest)**.
required: true
- label: I have filled out all of the requested information in this form, including specific version numbers.
required: true
+14 -6
View File
@@ -26,20 +26,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Dependency Review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Check code format
run: ./gradlew spotlessCheck
@@ -48,16 +48,24 @@ jobs:
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
- name: Run unit tests
id: unit_tests
run: ./gradlew testReleaseUnitTest
- name: Upload test report
if: steps.unit_tests.outcome == 'failure'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: test-report-${{ github.sha }}
path: app/build/reports/tests/testReleaseUnitTest
- name: Upload APK
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
- name: Upload mapping
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release
+11 -11
View File
@@ -29,16 +29,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
- name: Build
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
@@ -65,7 +65,7 @@ jobs:
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
- name: Upload APK
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: mihon
path: |
@@ -83,16 +83,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-disabled: true
@@ -117,7 +117,7 @@ jobs:
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
- name: Upload APK
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: mihon-foss
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
@@ -130,7 +130,7 @@ jobs:
steps:
- name: Download all artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
merge-multiple: true
@@ -143,12 +143,12 @@ jobs:
mihon-foss
- name: Create GitHub Release
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: ${{ needs.get_tag.outputs.tag }}
name: Mihon ${{ needs.get_tag.outputs.tag }}
body: |
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving!
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you're upgrading from an earlier version. Consider [becoming a patron](https://www.patreon.com/mihon/membership) to help keep Mihon improving!
<!-->
+54 -1
View File
@@ -12,6 +12,57 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
## [Unreleased]
## [v0.19.4] - 2026-02-25
### Added
- Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673))
- Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833))
- Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851))
- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891))
- Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927))
- Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928))
### Improved
- Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266))
- Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832))
- Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874))
- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977))
- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2978))
### Changed
- Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
- Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675))
- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995))
- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906))
- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
### Fixed
- Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680))
- Fix shizuku installer not updating installed extensions ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2697](https://github.com/mihonapp/mihon/pull/2697))
- Fix mass migration not using the same search queries as individual migration ([@AntsyLich](https://github.com/AntsyLich)) ([#2736](https://github.com/mihonapp/mihon/pull/2736))
- Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784))
- Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779))
- Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837))
- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816))
- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2993](https://github.com/mihonapp/mihon/pull/2993))
- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2994](https://github.com/mihonapp/mihon/pull/2994))
- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000))
- Fix crash when downloading multiple chapters with Arabic locale ([@MajorTanya](https://github.com/MajorTanya)) ([#2676](https://github.com/mihonapp/mihon/pull/2676))
### Other
- Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836))
## [v0.19.3] - 2025-11-07
### Improved
- Improved various aspects of the WebView multi window support ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
### Removed
- Revert "Fix reader tap zones triggering after scrolling was stopped by the user" due to introduction of regression ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
### Fixed
- Fix WebView crash introduced in 0.19.2 ([@bapeey](https://github.com/bapeey)) ([#2649](https://github.com/mihonapp/mihon/pull/2649))
- Fix extra padding appearing in reader after user interactions ([@AntsyLich](https://github.com/AntsyLich)) ([#2669](https://github.com/mihonapp/mihon/pull/2669))
- Fix long strip reader not scrolling on consecutive taps ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
## [v0.19.2] - 2025-11-02
### Added
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
@@ -433,7 +484,9 @@ 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.19.2...main
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.4...main
[v0.19.4]: https://github.com/mihonapp/mihon/compare/v0.19.3...v0.19.4
[v0.19.3]: https://github.com/mihonapp/mihon/compare/v0.19.2...v0.19.3
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
+5 -2
View File
@@ -26,8 +26,8 @@ android {
defaultConfig {
applicationId = "app.mihon"
versionCode = 15
versionName = "0.19.2"
versionCode = 17
versionName = "0.19.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -159,12 +159,14 @@ kotlin {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
"-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",
"-Xannotation-default-target=param-property",
)
}
}
@@ -270,6 +272,7 @@ dependencies {
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
implementation(libs.materialKolor)
// Logging
implementation(libs.logcat)
@@ -57,6 +57,7 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.interactor.SetSortModeForCategory
import tachiyomi.domain.category.interactor.UpdateCategory
import tachiyomi.domain.category.repository.CategoryRepository
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@@ -157,6 +158,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) }
addFactory { GetChaptersByMangaId(get()) }
addFactory { GetBookmarkedChaptersByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) }
@@ -4,11 +4,11 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.shouldExpandFAB
@@ -18,7 +18,7 @@ fun CategoryFloatingActionButton(
onCreate: () -> Unit,
modifier: Modifier = Modifier,
) {
ExtendedFloatingActionButton(
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
onClick = onCreate,
@@ -21,8 +21,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
@@ -195,7 +196,7 @@ fun AppBarActions(
actions.filterIsInstance<AppBar.Action>().map {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
PlainTooltip {
Text(it.title)
@@ -220,7 +221,7 @@ fun AppBarActions(
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
if (overflowActions.isNotEmpty()) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_menu_overflow_description))
@@ -349,7 +350,7 @@ fun SearchToolbar(
// Don't show search action
} else if (searchQuery == null) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_search))
@@ -369,7 +370,7 @@ fun SearchToolbar(
}
} else if (searchQuery.isNotEmpty()) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
positionProvider = rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = {
PlainTooltip {
Text(stringResource(MR.strings.action_reset))
@@ -22,7 +22,7 @@ fun relativeDateText(
Instant.ofEpochMilli(dateEpochMillis),
ZoneId.systemDefault(),
)
.takeIf { dateEpochMillis > 0L },
.takeIf { dateEpochMillis != 0L },
)
}
@@ -1,6 +1,5 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -14,11 +13,11 @@ import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun DownloadDropdownMenu(
modifier: Modifier = Modifier,
expanded: Boolean,
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
offset: DpOffset? = null,
modifier: Modifier = Modifier,
) {
if (offset != null) {
DropdownMenu(
@@ -49,7 +48,7 @@ fun DownloadDropdownMenu(
}
@Composable
private fun ColumnScope.DownloadDropdownMenuItems(
private fun DownloadDropdownMenuItems(
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
) {
@@ -59,6 +58,7 @@ private fun ColumnScope.DownloadDropdownMenuItems(
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
)
options.map { (downloadAction, string) ->
@@ -1,10 +1,7 @@
package eu.kanade.presentation.manga
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -27,9 +24,11 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -71,7 +70,6 @@ import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.TwoPanelBox
import tachiyomi.presentation.core.components.VerticalFastScroller
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.PullRefresh
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@@ -124,7 +122,7 @@ fun MangaScreen(
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
@@ -255,7 +253,7 @@ private fun MangaScreenSmallImpl(
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
@@ -330,25 +328,23 @@ private fun MangaScreenSmallImpl(
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
}
SmallExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(if (isReading) MR.strings.action_resume else MR.strings.action_start),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
modifier = Modifier.animateFloatingActionButton(
visible = isFABVisible,
alignment = Alignment.BottomEnd,
),
)
},
) { contentPadding ->
val topPadding = contentPadding.calculateTopPadding()
@@ -499,7 +495,7 @@ fun MangaScreenLargeImpl(
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
// Chapter selection
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
onAllChapterSelected: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
) {
@@ -572,27 +568,25 @@ fun MangaScreenLargeImpl(
val isFABVisible = remember(chapters) {
chapters.fastAny { !it.chapter.read } && !isAnySelected
}
AnimatedVisibility(
visible = isFABVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
ExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
)
}
SmallExtendedFloatingActionButton(
text = {
val isReading = remember(state.chapters) {
state.chapters.fastAny { it.chapter.read }
}
Text(
text = stringResource(
if (isReading) MR.strings.action_resume else MR.strings.action_start,
),
)
},
icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) },
onClick = onContinueReading,
expanded = chapterListState.shouldExpandFAB(),
modifier = Modifier.animateFloatingActionButton(
visible = isFABVisible,
alignment = Alignment.BottomEnd,
),
)
},
) { contentPadding ->
PullRefresh(
@@ -745,7 +739,7 @@ private fun LazyListScope.sharedChapterItems(
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit,
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
) {
items(
@@ -793,14 +787,14 @@ private fun LazyListScope.sharedChapterItems(
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onLongClick = {
onChapterSelected(item, !item.selected, true, true)
onChapterSelected(item, !item.selected, true)
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
onClick = {
onChapterItemClick(
chapterItem = item,
isAnyChapterSelected = isAnyChapterSelected,
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
onChapterClicked = onChapterClicked,
)
},
@@ -6,6 +6,7 @@ enum class DownloadAction {
NEXT_10_CHAPTERS,
NEXT_25_CHAPTERS,
UNREAD_CHAPTERS,
BOOKMARKED_CHAPTERS,
}
enum class EditCoverAction {
@@ -91,10 +91,10 @@ fun MangaBottomActionMenu(
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false, false, false) }
var resetJob: Job? = remember { null }
var resetJob by remember { mutableStateOf<Job?>(null) }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..<7).forEach { i -> confirm[i] = i == toConfirmIndex }
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
@@ -252,10 +252,10 @@ fun LibraryBottomActionMenu(
) {
val haptic = LocalHapticFeedback.current
val confirm = remember { mutableStateListOf(false, false, false, false, false, false) }
var resetJob: Job? = remember { null }
var resetJob by remember { mutableStateOf<Job?>(null) }
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
(0..5).forEach { i -> confirm[i] = i == toConfirmIndex }
confirm.indices.forEach { i -> confirm[i] = i == toConfirmIndex }
resetJob?.cancel()
resetJob = scope.launch {
delay(1.seconds)
@@ -567,44 +567,47 @@ private fun ColumnScope.MangaContentInfo(
}
}
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator(
annotate = { content, child ->
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
@Composable
private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = remember(loadImages, linkStyle) {
markdownAnnotator(
annotate = { content, child ->
if (!loadImages && child.type == MarkdownElementTypes.IMAGE) {
val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK)
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
?.getUnescapedTextInNode(content)
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
?.getUnescapedTextInNode(content)
?: return@markdownAnnotator false
?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK)
?.findChildOfType(MarkdownTokenTypes.AUTOLINK)
?.getUnescapedTextInNode(content)
?: return@markdownAnnotator false
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
?.getUnescapedTextInNode(content).orEmpty()
val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE)
?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT)
val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT)
?.getUnescapedTextInNode(content).orEmpty()
withLink(LinkAnnotation.Url(url = url)) {
pushStyle(linkStyle)
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
append(altText)
pop()
withLink(LinkAnnotation.Url(url = url)) {
pushStyle(linkStyle)
appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG)
append(altText)
pop()
}
return@markdownAnnotator true
}
return@markdownAnnotator true
}
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
append(content.substring(child.startOffset, child.endOffset))
return@markdownAnnotator true
}
if (child.type in DISALLOWED_MARKDOWN_TYPES) {
append(content.substring(child.startOffset, child.endOffset))
return@markdownAnnotator true
}
false
},
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
false
},
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
}
@Composable
private fun MangaSummary(
@@ -225,7 +225,6 @@ object AboutScreen : Screen() {
is GetApplicationRelease.Result.OsTooOld -> {
context.toast(MR.strings.update_check_eol)
}
else -> {}
}
} catch (e: Exception) {
context.toast(e.message)
@@ -2,13 +2,16 @@ package eu.kanade.presentation.more.settings.screen.about
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
@@ -27,7 +30,9 @@ class OpenSourceLicensesScreen : Screen() {
)
},
) { contentPadding ->
val libraries by produceLibraries(R.raw.aboutlibraries)
LibrariesContainer(
libraries = libraries,
modifier = Modifier
.fillMaxSize(),
contentPadding = contentPadding,
@@ -1,6 +1,7 @@
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
@@ -61,6 +62,7 @@ fun ExtensionRepoCreateDialog(
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
@@ -28,7 +28,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
pref = screenModel.preferences.customBrightness(),
)
/**
/*
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
@@ -1,10 +1,11 @@
package eu.kanade.presentation.theme
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
@@ -53,26 +54,36 @@ private fun BaseTachiyomiTheme(
isAmoled: Boolean,
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = getThemeColorScheme(appTheme, isAmoled),
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
MaterialExpressiveTheme(
colorScheme = remember(appTheme, isDark, isAmoled) {
getThemeColorScheme(
context = context,
appTheme = appTheme,
isDark = isDark,
isAmoled = isAmoled,
)
},
content = content,
)
}
@Composable
@ReadOnlyComposable
private fun getThemeColorScheme(
context: Context,
appTheme: AppTheme,
isDark: Boolean,
isAmoled: Boolean,
): ColorScheme {
val colorScheme = if (appTheme == AppTheme.MONET) {
MonetColorScheme(LocalContext.current)
MonetColorScheme(context)
} else {
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
}
return colorScheme.getColorScheme(
isSystemInDarkTheme(),
isAmoled,
isDark = isDark,
isAmoled = isAmoled,
overrideDarkSurfaceContainers = appTheme != AppTheme.MONET,
)
}
@@ -14,16 +14,25 @@ internal abstract class BaseColorScheme {
private val surfaceContainerHigh = Color(0xFF131313)
private val surfaceContainerHighest = Color(0xFF1B1B1B)
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
fun getColorScheme(
isDark: Boolean,
isAmoled: Boolean,
overrideDarkSurfaceContainers: Boolean,
): ColorScheme {
if (!isDark) return lightScheme
if (!isAmoled) return darkScheme
return darkScheme.copy(
val amoledScheme = darkScheme.copy(
background = Color.Black,
onBackground = Color.White,
surface = Color.Black,
onSurface = Color.White,
)
if (!overrideDarkSurfaceContainers) return amoledScheme
return amoledScheme.copy(
surfaceVariant = surfaceContainer, // Navigation bar background (ThemePrefWidget)
surfaceContainerLowest = surfaceContainer,
surfaceContainerLow = surfaceContainer,
@@ -1,22 +1,17 @@
package eu.kanade.presentation.theme.colorscheme
import android.annotation.SuppressLint
import android.app.UiModeManager
import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.graphics.Color
import androidx.core.content.getSystemService
import com.google.android.material.color.utilities.Hct
import com.google.android.material.color.utilities.MaterialDynamicColors
import com.google.android.material.color.utilities.QuantizerCelebi
import com.google.android.material.color.utilities.SchemeContent
import com.google.android.material.color.utilities.Score
import com.materialkolor.PaletteStyle
import com.materialkolor.dynamiccolor.ColorSpec
import com.materialkolor.ktx.DynamicScheme
import com.materialkolor.toColorScheme
internal class MonetColorScheme(context: Context) : BaseColorScheme() {
@@ -28,7 +23,7 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
?.primaryColor
?.toArgb()
if (seed != null) {
MonetCompatColorScheme(context, seed)
MonetCompatColorScheme(Color(seed))
} else {
TachiyomiColorScheme
}
@@ -41,19 +36,6 @@ internal class MonetColorScheme(context: Context) : BaseColorScheme() {
override val lightScheme
get() = monet.lightScheme
companion object {
@Suppress("Unused")
@SuppressLint("RestrictedApi")
fun extractSeedColorFromImage(bitmap: Bitmap): Int? {
val width = bitmap.width
val height = bitmap.height
val bitmapPixels = IntArray(width * height)
bitmap.getPixels(bitmapPixels, 0, width, 0, 0, width, height)
return Score.score(QuantizerCelebi.quantize(bitmapPixels, 128), 1, 0)[0]
.takeIf { it != 0 } // Don't take fallback color
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
@@ -62,64 +44,19 @@ private class MonetSystemColorScheme(context: Context) : BaseColorScheme() {
override val darkScheme = dynamicDarkColorScheme(context)
}
private class MonetCompatColorScheme(context: Context, seed: Int) : BaseColorScheme() {
override val lightScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = false)
override val darkScheme = generateColorSchemeFromSeed(context = context, seed = seed, dark = true)
internal class MonetCompatColorScheme(seed: Color) : BaseColorScheme() {
override val lightScheme = generateColorSchemeFromSeed(seed = seed, dark = false)
override val darkScheme = generateColorSchemeFromSeed(seed = seed, dark = true)
companion object {
private fun Int.toComposeColor(): Color = Color(this)
@SuppressLint("PrivateResource", "RestrictedApi")
private fun generateColorSchemeFromSeed(context: Context, seed: Int, dark: Boolean): ColorScheme {
val scheme = SchemeContent(
Hct.fromInt(seed),
dark,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context.getSystemService<UiModeManager>()?.contrast?.toDouble() ?: 0.0
} else {
0.0
},
)
val dynamicColors = MaterialDynamicColors()
return ColorScheme(
primary = dynamicColors.primary().getArgb(scheme).toComposeColor(),
onPrimary = dynamicColors.onPrimary().getArgb(scheme).toComposeColor(),
primaryContainer = dynamicColors.primaryContainer().getArgb(scheme).toComposeColor(),
onPrimaryContainer = dynamicColors.onPrimaryContainer().getArgb(scheme).toComposeColor(),
inversePrimary = dynamicColors.inversePrimary().getArgb(scheme).toComposeColor(),
secondary = dynamicColors.secondary().getArgb(scheme).toComposeColor(),
onSecondary = dynamicColors.onSecondary().getArgb(scheme).toComposeColor(),
secondaryContainer = dynamicColors.secondaryContainer().getArgb(scheme).toComposeColor(),
onSecondaryContainer = dynamicColors.onSecondaryContainer().getArgb(scheme).toComposeColor(),
tertiary = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiary = dynamicColors.onTertiary().getArgb(scheme).toComposeColor(),
tertiaryContainer = dynamicColors.tertiary().getArgb(scheme).toComposeColor(),
onTertiaryContainer = dynamicColors.onTertiaryContainer().getArgb(scheme).toComposeColor(),
background = dynamicColors.background().getArgb(scheme).toComposeColor(),
onBackground = dynamicColors.onBackground().getArgb(scheme).toComposeColor(),
surface = dynamicColors.surface().getArgb(scheme).toComposeColor(),
onSurface = dynamicColors.onSurface().getArgb(scheme).toComposeColor(),
surfaceVariant = dynamicColors.surfaceVariant().getArgb(scheme).toComposeColor(),
onSurfaceVariant = dynamicColors.onSurfaceVariant().getArgb(scheme).toComposeColor(),
surfaceTint = dynamicColors.surfaceTint().getArgb(scheme).toComposeColor(),
inverseSurface = dynamicColors.inverseSurface().getArgb(scheme).toComposeColor(),
inverseOnSurface = dynamicColors.inverseOnSurface().getArgb(scheme).toComposeColor(),
error = dynamicColors.error().getArgb(scheme).toComposeColor(),
onError = dynamicColors.onError().getArgb(scheme).toComposeColor(),
errorContainer = dynamicColors.errorContainer().getArgb(scheme).toComposeColor(),
onErrorContainer = dynamicColors.onErrorContainer().getArgb(scheme).toComposeColor(),
outline = dynamicColors.outline().getArgb(scheme).toComposeColor(),
outlineVariant = dynamicColors.outlineVariant().getArgb(scheme).toComposeColor(),
scrim = Color.Black,
surfaceBright = dynamicColors.surfaceBright().getArgb(scheme).toComposeColor(),
surfaceDim = dynamicColors.surfaceDim().getArgb(scheme).toComposeColor(),
surfaceContainer = dynamicColors.surfaceContainer().getArgb(scheme).toComposeColor(),
surfaceContainerHigh = dynamicColors.surfaceContainerHigh().getArgb(scheme).toComposeColor(),
surfaceContainerHighest = dynamicColors.surfaceContainerHighest().getArgb(scheme).toComposeColor(),
surfaceContainerLow = dynamicColors.surfaceContainerLow().getArgb(scheme).toComposeColor(),
surfaceContainerLowest = dynamicColors.surfaceContainerLowest().getArgb(scheme).toComposeColor(),
fun generateColorSchemeFromSeed(seed: Color, dark: Boolean): ColorScheme {
return DynamicScheme(
seedColor = seed,
isDark = dark,
specVersion = ColorSpec.SpecVersion.SPEC_2025,
style = PaletteStyle.Expressive,
)
.toColorScheme(isAmoled = false)
}
}
}
@@ -1,5 +1,6 @@
package eu.kanade.presentation.track
import android.content.ClipData
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -47,6 +48,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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
@@ -55,11 +57,11 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.platform.toClipEntry
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.intl.Locale
@@ -73,6 +75,7 @@ import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.system.openInBrowser
import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
@@ -240,7 +243,7 @@ private fun SearchResultItem(
onClick: () -> Unit,
) {
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val clipboard: Clipboard = LocalClipboard.current
val focusManager = LocalFocusManager.current
val type = trackSearch.publishing_type.toLowerCase(Locale.current).capitalize(Locale.current)
val status = trackSearch.publishing_status.toLowerCase(Locale.current).capitalize(Locale.current)
@@ -248,6 +251,7 @@ private fun SearchResultItem(
val shape = RoundedCornerShape(16.dp)
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
var dropDownMenuExpanded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxWidth()
@@ -295,7 +299,13 @@ private fun SearchResultItem(
expanded = dropDownMenuExpanded,
onCollapseMenu = { dropDownMenuExpanded = false },
onCopyName = {
clipboardManager.setText(AnnotatedString(trackSearch.title))
scope.launch {
val clipEntry = ClipData.newPlainText(
trackSearch.title,
trackSearch.title,
).toClipEntry()
clipboard.setClipEntry(clipEntry)
}
},
onOpenInBrowser = {
val url = trackSearch.tracking_url
@@ -1,15 +1,11 @@
package eu.kanade.presentation.track.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -30,18 +26,13 @@ fun TrackLogoIcon(
Modifier
}
Box(
Image(
painter = painterResource(tracker.getLogo()),
contentDescription = tracker.name,
modifier = modifier
.size(48.dp)
.background(color = Color(tracker.getLogoColor()), shape = MaterialTheme.shapes.medium)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
Image(
painter = painterResource(tracker.getLogo()),
contentDescription = tracker.name,
)
}
.clip(MaterialTheme.shapes.medium),
)
}
@PreviewLightDark
@@ -1,8 +1,6 @@
package eu.kanade.presentation.track.components
import android.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.test.DummyTracker
@@ -13,8 +11,6 @@ internal class TrackLogoIconPreviewProvider : PreviewParameterProvider<Tracker>
DummyTracker(
id = 1L,
name = "Dummy Tracker",
valLogoColor = Color.rgb(18, 25, 35),
valLogo = R.drawable.ic_tracker_anilist,
),
)
}
@@ -0,0 +1,111 @@
package eu.kanade.presentation.updates
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.updates.service.UpdatesPreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@Composable
fun UpdatesFilterDialog(
onDismissRequest: () -> Unit,
screenModel: UpdatesSettingsScreenModel,
) {
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = persistentListOf(
stringResource(MR.strings.action_filter),
),
) {
Column(
modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
FilterSheet(screenModel = screenModel)
}
}
}
@Composable
private fun ColumnScope.FilterSheet(
screenModel: UpdatesSettingsScreenModel,
) {
val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_downloaded),
state = filterDownloaded,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) },
)
val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_unread),
state = filterUnread,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) },
)
val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState()
TriStateItem(
label = stringResource(MR.strings.label_started),
state = filterStarted,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) },
)
val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState()
TriStateItem(
label = stringResource(MR.strings.action_filter_bookmarked),
state = filterBookmarked,
onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) },
)
HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small))
val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState()
fun toggleScanlatorFilter() = screenModel.updatesPreferences.filterExcludedScanlators().getAndSet { !it }
Row(
modifier = Modifier
.clickable { toggleScanlatorFilter() }
.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = stringResource(MR.strings.action_filter_excluded_scanlators),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
)
Switch(
checked = filterExcludedScanlators,
onCheckedChange = { toggleScanlatorFilter() },
)
}
}
@@ -5,9 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarScrollBehavior
@@ -37,6 +40,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.active
import java.time.LocalDate
import kotlin.time.Duration.Companion.seconds
@@ -54,8 +58,10 @@ fun UpdateScreen(
onMultiBookmarkClicked: (List<UpdatesItem>, bookmark: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List<UpdatesItem>, read: Boolean) -> Unit,
onMultiDeleteClicked: (List<UpdatesItem>) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
onOpenChapter: (UpdatesItem) -> Unit,
onFilterClicked: () -> Unit,
hasActiveFilters: Boolean,
) {
BackHandler(enabled = state.selectionMode) {
onSelectAll(false)
@@ -66,6 +72,8 @@ fun UpdateScreen(
UpdatesAppBar(
onCalendarClicked = { onCalendarClicked() },
onUpdateLibrary = { onUpdateLibrary() },
onFilterClicked = { onFilterClicked() },
hasFilters = hasActiveFilters,
actionModeCounter = state.selected.size,
onSelectAll = { onSelectAll(true) },
onInvertSelection = { onInvertSelection() },
@@ -133,6 +141,8 @@ fun UpdateScreen(
private fun UpdatesAppBar(
onCalendarClicked: () -> Unit,
onUpdateLibrary: () -> Unit,
onFilterClicked: () -> Unit,
hasFilters: Boolean,
// For action mode
actionModeCounter: Int,
onSelectAll: () -> Unit,
@@ -147,6 +157,12 @@ private fun UpdatesAppBar(
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList,
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
onClick = onFilterClicked,
),
AppBar.Action(
title = stringResource(MR.strings.action_view_upcoming),
icon = Icons.Outlined.CalendarMonth,
@@ -69,7 +69,7 @@ internal fun LazyListScope.updatesLastUpdatedItem(
internal fun LazyListScope.updatesUiItems(
uiModels: List<UpdatesUiModel>,
selectionMode: Boolean,
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
onUpdateSelected: (UpdatesItem, Boolean, Boolean) -> Unit,
onClickCover: (UpdatesItem) -> Unit,
onClickUpdate: (UpdatesItem) -> Unit,
onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit,
@@ -111,11 +111,11 @@ internal fun LazyListScope.updatesUiItems(
)
},
onLongClick = {
onUpdateSelected(updatesItem, !updatesItem.selected, true, true)
onUpdateSelected(updatesItem, !updatesItem.selected, true)
},
onClick = {
when {
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, true, false)
selectionMode -> onUpdateSelected(updatesItem, !updatesItem.selected, false)
else -> onClickUpdate(updatesItem)
}
},
@@ -9,21 +9,21 @@ import tachiyomi.domain.source.model.SourceNotInstalledException
import tachiyomi.i18n.MR
import java.net.UnknownHostException
context(Context)
context(context: Context)
val Throwable.formattedMessage: String
get() {
when (this) {
is HttpException -> return stringResource(MR.strings.exception_http, code)
is HttpException -> return context.stringResource(MR.strings.exception_http, code)
is UnknownHostException -> {
return if (!isOnline()) {
stringResource(MR.strings.exception_offline)
return if (!context.isOnline()) {
context.stringResource(MR.strings.exception_offline)
} else {
stringResource(MR.strings.exception_unknown_host, message ?: "")
context.stringResource(MR.strings.exception_unknown_host, message ?: "")
}
}
is NoResultsException -> return stringResource(MR.strings.no_results_found)
is SourceNotInstalledException -> return stringResource(MR.strings.loader_not_implemented_error)
is NoResultsException -> return context.stringResource(MR.strings.no_results_found)
is SourceNotInstalledException -> return context.stringResource(MR.strings.loader_not_implemented_error)
}
return when (val className = this::class.simpleName) {
"Exception", "IOException" -> message ?: className
@@ -4,5 +4,7 @@ import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.ui.Modifier
// https://issuetracker.google.com/352584409
context(LazyItemScope)
fun Modifier.animateItemFastScroll() = this.animateItem(fadeInSpec = null, fadeOutSpec = null)
context(itemScope: LazyItemScope)
fun Modifier.animateItemFastScroll() = with(itemScope) {
this@animateItemFastScroll.animateItem(fadeInSpec = null, fadeOutSpec = null)
}
@@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.os.Message
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -28,7 +29,9 @@ 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.stack.mutableStateStackOf
import com.kevinnzou.web.AccompanistWebChromeClient
@@ -36,12 +39,13 @@ import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.LoadingState
import com.kevinnzou.web.WebContent
import com.kevinnzou.web.WebView
import com.kevinnzou.web.WebViewNavigator
import com.kevinnzou.web.WebViewState
import com.kevinnzou.web.rememberWebViewNavigator
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.R
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf
@@ -50,13 +54,13 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
class WebViewWindow(webContent: WebContent) {
class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
var state by mutableStateOf(WebViewState(webContent))
var popupMessage: Message? = null
private set
var webView: WebView? = null
constructor(popupMessage: Message) : this(WebContent.NavigatorOnly) {
constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
this.popupMessage = popupMessage
}
}
@@ -72,27 +76,20 @@ fun WebViewScreenContent(
headers: Map<String, String> = emptyMap(),
onUrlChange: (String) -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val windowStack = remember {
mutableStateStackOf(
WebViewWindow(
WebContent.Url(url = url, additionalHttpHeaders = headers),
WebViewNavigator(coroutineScope),
),
)
}
val currentWindow = windowStack.lastItemOrNull!!
val navigator = currentWindow.navigator
val popState: (() -> Unit) = remember {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
val navigator = rememberWebViewNavigator()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
@@ -133,21 +130,20 @@ fun WebViewScreenContent(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
request?.let {
// Don't attempt to open blobs as webpages
if (it.url.toString().startsWith("blob:http")) {
return false
}
val url = request?.url?.toString() ?: return false
// Ignore intents urls
if (it.url.toString().startsWith("intent://")) {
// Ignore intents urls
if (url.startsWith("intent://")) return true
// Only open valid web urls
if (url.startsWith("http") || url.startsWith("https")) {
if (url != view?.url) {
view?.loadUrl(url, headers)
return true
}
// Continue with request, but with custom headers
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
return false
}
}
}
@@ -162,7 +158,7 @@ fun WebViewScreenContent(
): Boolean {
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
if (isUserGesture) {
windowStack.push(WebViewWindow(resultMsg))
windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
return true
}
return false
@@ -177,6 +173,18 @@ fun WebViewScreenContent(
return webView
}
val popState = remember<() -> Unit> {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
BackHandler(windowStack.size > 1, popState)
Scaffold(
topBar = {
Box {
@@ -184,7 +192,7 @@ fun WebViewScreenContent(
AppBar(
title = currentWindow.state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = popState,
navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
@@ -225,7 +233,18 @@ fun WebViewScreenContent(
title = stringResource(MR.strings.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
).builder().apply {
if (windowStack.size > 1) {
add(
0,
AppBar.Action(
title = stringResource(MR.strings.action_webview_close_tab),
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
onClick = popState,
),
)
}
}.build(),
)
},
)
@@ -254,7 +273,7 @@ fun WebViewScreenContent(
.align(Alignment.BottomCenter),
)
is LoadingState.Loading -> LinearProgressIndicator(
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
progress = { loadingState.progress },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
@@ -298,7 +317,7 @@ fun WebViewScreenContent(
// The composable is being disposed but the WebView object is not.
// When the WebView element is recomposed, we will want the WebView to resume from its state
// before it was unmounted, we won't want it to reset back to its original target.
window.state = WebViewState(WebContent.NavigatorOnly)
window.state.content = WebContent.NavigatorOnly
}
},
client = webClient,
+15 -2
View File
@@ -17,6 +17,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.request.crossfade
@@ -157,8 +158,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
// Updates widget update
WidgetManager(Injekt.get(), Injekt.get()).apply { init(scope) }
if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
if (!LogcatLogger.isInstalled) {
val minLogPriority = when {
networkPreferences.verboseLogging().get() -> LogPriority.VERBOSE
BuildConfig.DEBUG -> LogPriority.DEBUG
else -> LogPriority.INFO
}
LogcatLogger.install()
LogcatLogger.loggers += AndroidLogcatLogger(minLogPriority)
}
initializeMigrator()
@@ -196,6 +203,12 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
add(MangaKeyer())
}
memoryCache(
MemoryCache.Builder()
.maxSizePercent(context)
.build(),
)
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
@@ -418,10 +418,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*/
private const val KEY_CATEGORY = "category"
fun cancelAllWorks(context: Context) {
context.workManager.cancelAllWorkByTag(TAG)
}
fun setupTask(
context: Context,
prefInterval: Int? = null,
@@ -435,16 +431,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} 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 networkRequest = NetworkRequest.Builder().apply {
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
if (DEVICE_ONLY_ON_WIFI in restrictions) {
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
}
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
}
}
.build()
val constraints = Constraints.Builder()
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
.setRequiredNetworkRequest(networkRequest, networkType)
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.setRequiresBatteryNotLow(true)
.build()
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.track
import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.data.database.models.Track
@@ -24,9 +23,6 @@ interface Tracker {
val supportsPrivateTracking: Boolean
@ColorInt
fun getLogoColor(): Int
@DrawableRes
fun getLogo(): Int
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.anilist
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.R
@@ -56,9 +55,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
}
}
override fun getLogo() = R.drawable.ic_tracker_anilist
override fun getLogoColor() = Color.rgb(18, 25, 35)
override fun getLogo() = R.drawable.brand_anilist
override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
@@ -41,9 +41,9 @@ 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, ${'$'}private: Boolean) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
val query = $$"""
|mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) {
|SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) {
| id
| status
|}
@@ -78,14 +78,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
val query = """
val query = $$"""
|mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean,
|$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|id: $listId, progress: $progress, status: $status, private: $private,
|scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt
|) {
|id
|status
@@ -114,9 +114,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun deleteLibManga(track: DomainTrack) {
withIOContext {
val query = """
|mutation DeleteManga(${'$'}listId: Int) {
|DeleteMediaListEntry(id: ${'$'}listId) {
val query = $$"""
|mutation DeleteManga($listId: Int) {
|DeleteMediaListEntry(id: $listId) {
|deleted
|}
|}
@@ -135,10 +135,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val query = """
|query Search(${'$'}query: String) {
val query = $$"""
|query Search($query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|media(search: $query, type: MANGA, format_not_in: [NOVEL]) {
|id
|staff {
|edges {
@@ -197,10 +197,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext {
val query = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
val query = $$"""
|query ($id: Int!, $manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|mediaList(userId: $id, type: MANGA, mediaId: $manga_id) {
|id
|status
|scoreRaw: score(format: POINT_100)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.bangumi
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -79,9 +78,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
return track
}
override fun getLogo() = R.drawable.ic_tracker_bangumi
override fun getLogoColor() = Color.rgb(240, 145, 153)
override fun getLogo() = R.drawable.brand_bangumi
override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
@@ -111,7 +108,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
// 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()
val username = api.getUsername()
saveCredentials(username, oauth.accessToken)
} catch (_: Throwable) {
logout()
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.kavita
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -34,9 +33,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
private val sourceManager: SourceManager by injectLazy()
override fun getLogo(): Int = R.drawable.ic_tracker_kavita
override fun getLogoColor() = Color.rgb(74, 198, 148)
override fun getLogo(): Int = R.drawable.brand_kavita
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
@@ -140,7 +137,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
}
authentication.apiUrl = prefApiUrl
authentication.jwtToken = token.toString()
authentication.jwtToken = token
}
authentications = oauth
}
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.kitsu
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -36,9 +35,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
private val api by lazy { KitsuApi(client, interceptor) }
override fun getLogo() = R.drawable.ic_tracker_kitsu
override fun getLogoColor() = Color.rgb(51, 37, 50)
override fun getLogo() = R.drawable.brand_kitsu
override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.komga
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -31,9 +30,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
val api by lazy { KomgaApi(id, client) }
override fun getLogo() = R.drawable.ic_tracker_komga
override fun getLogoColor() = Color.rgb(51, 37, 50)
override fun getLogo() = R.drawable.brand_komga
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.mangaupdates
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -42,9 +41,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
private val api by lazy { MangaUpdatesApi(interceptor, client) }
override fun getLogo(): Int = R.drawable.ic_manga_updates
override fun getLogoColor(): Int = Color.rgb(146, 160, 173)
override fun getLogo(): Int = R.drawable.brand_mangaupdates
override fun getStatusList(): List<Long> {
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -10,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
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
@@ -41,9 +39,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
override val supportsReadingDates: Boolean = true
override fun getLogo() = R.drawable.ic_tracker_mal
override fun getLogoColor() = Color.rgb(46, 81, 162)
override fun getLogo() = R.drawable.brand_myanimelist
override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
@@ -10,15 +10,12 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALManga
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUserSearchResult
import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.PkceUtil
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
@@ -78,15 +75,15 @@ class MyAnimeListApi(
// MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true")
.appendQueryParameter("fields", SEARCH_FIELDS)
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALSearchResult>()
.data
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.filter { !it.publishing_type.contains("novel") }
.filter { !(it.node.mediaType.contains("novel")) }
.map { parseSearchItem(it.node) }
}
}
}
@@ -95,29 +92,13 @@ class MyAnimeListApi(
return withIOContext {
val url = "$BASE_API_URL/manga".toUri().buildUpon()
.appendPath(id.toString())
.appendQueryParameter(
"fields",
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
)
.appendQueryParameter("fields", SEARCH_FIELDS)
.build()
with(json) {
authClient.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<MALManga>()
.let {
TrackSearch.create(trackId).apply {
remote_id = it.id
title = it.title
summary = it.synopsis
total_chapters = it.numChapters
score = it.mean
cover_url = it.covers?.large.orEmpty()
tracking_url = "https://myanimelist.net/manga/$remote_id"
publishing_status = it.status.replace("_", " ")
publishing_type = it.mediaType.replace("_", " ")
start_date = it.startDate ?: ""
}
}
.let { parseSearchItem(it) }
}
}
}
@@ -181,8 +162,7 @@ class MyAnimeListApi(
val matches = myListSearchResult.data
.filter { it.node.title.contains(query, ignoreCase = true) }
.map { async { getMangaDetails(it.node.id) } }
.awaitAll()
.map { parseSearchItem(it.node) }
// Check next page if there's more
if (!myListSearchResult.paging.next.isNullOrBlank()) {
@@ -193,10 +173,10 @@ class MyAnimeListApi(
}
}
private suspend fun getListPage(offset: Int): MALUserSearchResult {
private suspend fun getListPage(offset: Int): MALSearchResult {
return withIOContext {
val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
.appendQueryParameter("fields", SEARCH_FIELDS)
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
if (offset > 0) {
urlBuilder.appendQueryParameter("offset", offset.toString())
@@ -225,6 +205,28 @@ class MyAnimeListApi(
}
}
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
return TrackSearch.create(trackId).apply {
remote_id = searchItem.id
title = searchItem.title
summary = searchItem.synopsis
total_chapters = searchItem.numChapters
score = searchItem.mean
cover_url = searchItem.covers?.large.orEmpty()
tracking_url = "https://myanimelist.net/manga/$remote_id"
publishing_status = searchItem.status.replace("_", " ")
publishing_type = searchItem.mediaType.replace("_", " ")
start_date = searchItem.startDate ?: ""
artists = searchItem.authors
.filter { authorNode -> authorNode.role == "Art" }
.mapNotNull { authorNode -> authorNode.node.getFullName() }
authors = searchItem.authors
// count all with "Story" or "Story & Art" as authors, like is done for library entries
.filter { authorNode -> authorNode.role.contains("Story") }
.mapNotNull { authorNode -> authorNode.node.getFullName() }
}
}
private fun parseDate(isoDate: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
}
@@ -236,7 +238,7 @@ class MyAnimeListApi(
return try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(epochTime)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
@@ -247,6 +249,9 @@ class MyAnimeListApi(
private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2"
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
private const val SEARCH_FIELDS =
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date,authors{first_name,last_name}"
private const val LIST_PAGINATION_AMOUNT = 250
private var codeVerifier: String = ""
@@ -18,8 +18,26 @@ data class MALManga(
val mediaType: String,
@SerialName("start_date")
val startDate: String?,
val authors: List<MALAuthorNode> = emptyList(),
)
@Serializable
data class MALAuthorNode(
val node: MALAuthor,
val role: String,
)
@Serializable
data class MALAuthor(
val id: Int,
@SerialName("first_name")
val firstName: String,
@SerialName("last_name")
val lastName: String,
) {
fun getFullName(): String? = "$firstName $lastName".trim().ifBlank { null }
}
@Serializable
data class MALMangaCovers(
val large: String = "",
@@ -5,14 +5,15 @@ import kotlinx.serialization.Serializable
@Serializable
data class MALSearchResult(
val data: List<MALSearchResultNode>,
val paging: MALSearchPaging,
)
@Serializable
data class MALSearchResultNode(
val node: MALSearchResultItem,
val node: MALManga,
)
@Serializable
data class MALSearchResultItem(
val id: Int,
data class MALSearchPaging(
val next: String?,
)
@@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.data.track.myanimelist.dto
import kotlinx.serialization.Serializable
@Serializable
data class MALUserSearchResult(
val data: List<MALUserSearchItem>,
val paging: MALUserSearchPaging,
)
@Serializable
data class MALUserSearchItem(
val node: MALUserSearchItemNode,
)
@Serializable
data class MALUserSearchPaging(
val next: String?,
)
@Serializable
data class MALUserSearchItemNode(
val id: Int,
val title: String,
)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.shikimori
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -10,7 +9,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
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
@@ -98,9 +96,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
return track
}
override fun getLogo() = R.drawable.ic_tracker_shikimori
override fun getLogoColor() = Color.rgb(40, 40, 40)
override fun getLogo() = R.drawable.brand_shikimori
override fun getStatusList(): List<Long> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
@@ -18,14 +17,15 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
val api by lazy { SuwayomiApi(id) }
override fun getLogo() = R.drawable.ic_tracker_suwayomi
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO
override fun getLogo() = R.drawable.brand_suwayomi
companion object {
const val UNREAD = 1L
const val READING = 2L
const val COMPLETED = 3L
private const val TRACKER_DELETE_KEY = "Tracker Delete"
private const val TRACKER_DELETE_DEFAULT = false
}
override fun getStatusList(): List<Long> = listOf(UNREAD, READING, COMPLETED)
@@ -58,7 +58,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
}
}
return api.updateProgress(track)
return api.updateProgress(track, getPrefTrackerDelete())
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
@@ -105,4 +105,9 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
private fun String.getMangaId(): Long =
this.substringAfterLast('/').toLong()
private fun getPrefTrackerDelete(): Boolean {
val preferences = api.sourcePreferences()
return preferences.getBoolean(TRACKER_DELETE_KEY, TRACKER_DELETE_DEFAULT)
}
}
@@ -1,12 +1,15 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import android.content.SharedPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.sourcePreferences
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.addAll
import kotlinx.serialization.json.buildJsonObject
@@ -29,19 +32,22 @@ class SuwayomiApi(private val trackId: Long) {
private val sourceManager: SourceManager by injectLazy()
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
private val configurableSource: ConfigurableSource by lazy { (sourceManager.get(sourceId) as ConfigurableSource) }
private val client: OkHttpClient by lazy { source.client }
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
public fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences()
suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext {
val query = """
|query GetManga(${'$'}mangaId: Int!) {
| manga(id: ${'$'}mangaId) {
val query = $$"""
|query GetManga($mangaId: Int!) {
| manga(id: $mangaId) {
| ...MangaFragment
| }
|}
|
|$MangaFragment
|$$MangaFragment
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
@@ -79,12 +85,14 @@ class SuwayomiApi(private val trackId: Long) {
}
}
suspend fun updateProgress(track: Track): Track {
suspend fun updateProgress(track: Track, deleteDownloadsOnServer: Boolean = false): Track {
val mangaId = track.remote_id
val chaptersQuery = """
|query GetMangaUnreadChapters(${'$'}mangaId: Int!) {
| chapters(condition: {mangaId: ${'$'}mangaId, isRead: false}) {
// TODO: Include a filter on the chapter number here
// Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query
val chaptersQuery = $$"""
|query GetMangaUnreadChapters($mangaId: Int!) {
| chapters(condition: {mangaId: $mangaId, isRead: false}) {
| nodes {
| id
| chapterNumber
@@ -110,18 +118,29 @@ class SuwayomiApi(private val trackId: Long) {
.data
.entry
.nodes
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read } }
.mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read + 0.001 } }
}
val markQuery = """
|mutation MarkChaptersRead(${'$'}chapters: [Int!]!) {
| updateChapters(input: {ids: ${'$'}chapters, patch: {isRead: true}}) {
| chapters {
| id
| }
| }
|}
""".trimMargin()
val markQuery = if (deleteDownloadsOnServer) {
$$"""
|mutation MarkChaptersRead($chapters: [Int!]!) {
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
| __typename
| }
| deleteDownloadedChapters(input: {ids: $chapters}) {
| __typename
| }
|}
""".trimMargin()
} else {
$$"""
|mutation MarkChaptersRead($chapters: [Int!]!) {
| updateChapters(input: {ids: $chapters, patch: {isRead: true}}) {
| __typename
| }
|}
""".trimMargin()
}
val markPayload = buildJsonObject {
put("query", markQuery)
putJsonObject("variables") {
@@ -140,12 +159,10 @@ class SuwayomiApi(private val trackId: Long) {
.awaitSuccess()
}
val trackQuery = """
|mutation TrackManga(${'$'}mangaId: Int!) {
| trackProgress(input: {mangaId: ${'$'}mangaId}) {
| trackRecords {
| lastChapterRead
| }
val trackQuery = $$"""
|mutation TrackManga($mangaId: Int!) {
| trackProgress(input: {mangaId: $mangaId}) {
| __typename
| }
|}
""".trimMargin()
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory
@@ -46,6 +47,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
LibraryPreferences(get())
}
addSingletonFactory {
UpdatesPreferences(get())
}
addSingletonFactory {
ReaderPreferences(get())
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.installer
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
@@ -82,6 +83,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
inputStream.copyTo(outputStream)
session.fsync(outputStream)
}
service.contentResolver.delete(entry.uri, null, null)
val intentSender = PendingIntent.getBroadcast(
service,
@@ -89,6 +91,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
Intent(INSTALL_ACTION).setPackage(service.packageName),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
).intentSender
@SuppressLint("RequestInstallPackagesPolicy")
session.commit(intentSender)
}
} catch (e: Exception) {
@@ -109,9 +109,10 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
override fun processEntry(entry: Entry) {
super.processEntry(entry)
try {
shellInterface?.install(
service.contentResolver.openAssetFileDescriptor(entry.uri, "r"),
)
service.contentResolver.openAssetFileDescriptor(entry.uri, "r").use {
shellInterface?.install(it)
}
service.contentResolver.delete(entry.uri, null, null)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
continueQueue(InstallStep.Error)
@@ -124,7 +125,13 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
override fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.unbindUserService(shizukuArgs, connection, true)
if (Shizuku.pingBinder()) {
try {
Shizuku.unbindUserService(shizukuArgs, connection, true)
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to unbind shizuku service" }
}
}
service.unregisterReceiver(receiver)
logcat { "ShizukuInstaller destroy" }
scope.cancel()
@@ -64,6 +64,11 @@ class ExtensionInstallActivity : Activity() {
}
}
override fun onDestroy() {
super.onDestroy()
intent.data?.let { contentResolver.delete(it, null, null) }
}
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<ExtensionManager>()
@@ -1,66 +1,48 @@
package eu.kanade.tachiyomi.extension.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import kotlinx.coroutines.delay
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withUIContext
import okhttp3.OkHttpClient
import okhttp3.Request
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.time.Duration.Companion.seconds
/**
* The installer which installs, updates and uninstalls the extensions.
*
* @param context The application context.
*/
internal class ExtensionInstaller(private val context: Context) {
/**
* The system's download manager
*/
private val downloadManager = context.getSystemService<DownloadManager>()!!
/**
* The broadcast receiver which listens to download completion events.
*/
private val downloadReceiver = DownloadCompletionReceiver()
/**
* The currently requested downloads, with the package name (unique id) as key, and the id
* returned by the download manager.
*/
private val activeDownloads = hashMapOf<String, Long>()
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
internal class ExtensionInstaller(
private val context: Context,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val activeJobs = mutableMapOf<String, Job>()
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
private val httpClient: OkHttpClient = Injekt.get<NetworkHelper>().client
/**
* Adds the given extension to the downloads queue and returns an observable containing its
* step in the installation process.
@@ -69,129 +51,86 @@ internal class ExtensionInstaller(private val context: Context) {
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
val pkgName = extension.pkgName
val downloadId = extension.pkgName.hashCode().toLong()
cancelInstall(extension.pkgName)
val oldDownload = activeDownloads[pkgName]
if (oldDownload != null) {
deleteDownload(pkgName)
}
val step = MutableStateFlow(InstallStep.Pending)
activeSteps[downloadId] = step
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val job = scope.launch {
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
try {
step.value = InstallStep.Downloading
val request = Request.Builder().url(url).build()
val response = httpClient.newCall(request).execute()
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
if (!response.isSuccessful) {
throw Exception("Failed to download extension")
}
response.body.byteStream().use { input ->
tmpFile.outputStream().use { output ->
input.copyTo(output)
}
}
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
downloadsStateFlows[id] = downloadStateFlow
// Poll download status
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
// Map to our model
when (downloadStatus) {
DownloadManager.STATUS_PENDING -> InstallStep.Pending
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
else -> null
step.value = InstallStep.Installing
installApk(downloadId, tmpFile)
} catch (e: Exception) {
if (e is InterruptedException) {
// Canceled
} else {
logcat(LogPriority.ERROR, e)
step.value = InstallStep.Error
}
}
}
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
emit(it)
// Stop when the application is installed or errors
!it.isCompleted()
}.onCompletion {
// Always notify on main thread
withUIContext {
// Always remove the download when unsubscribed
deleteDownload(pkgName)
activeJobs[extension.pkgName] = job
return step.asStateFlow()
.onCompletion {
activeJobs.remove(extension.pkgName)
activeSteps.remove(downloadId)
job.cancel()
}
}
}
/**
* Returns a flow that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
val query = DownloadManager.Query().setFilterById(id)
while (true) {
// Get the current download status
val downloadStatus = downloadManager.query(query).use { cursor ->
if (!cursor.moveToFirst()) return@flow
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
emit(downloadStatus)
// Stop polling when the download fails or finishes
if (
downloadStatus == DownloadManager.STATUS_SUCCESSFUL ||
downloadStatus == DownloadManager.STATUS_FAILED
) {
return@flow
}
delay(1.seconds)
}
}
// Ignore duplicate results
.distinctUntilChanged()
/**
* Starts an intent to install the extension at the given uri.
*
* @param uri The uri of the extension to install.
* @param tempFile The file of the extension to install. Delete after use.
*/
fun installApk(downloadId: Long, uri: Uri) {
private fun installApk(downloadId: Long, tempFile: File) {
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, ExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME)
.setDataAndType(tempFile.getUriCompat(context), APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
BasePreferences.ExtensionInstaller.PRIVATE -> {
val extensionManager = Injekt.get<ExtensionManager>()
val tempFile = File(context.cacheDir, "temp_$downloadId")
if (tempFile.exists() && !tempFile.delete()) {
// Unlikely but just in case
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
return
}
try {
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
updateInstallStep(downloadId, InstallStep.Installed)
} else {
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
updateInstallStep(downloadId, InstallStep.Error)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
updateInstallStep(downloadId, InstallStep.Error)
}
tempFile.delete()
}
else -> {
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
val intent = ExtensionInstallService.getIntent(
context,
downloadId,
tempFile.getUriCompat(context),
installer,
)
ContextCompat.startForegroundService(context, intent)
}
}
@@ -201,9 +140,8 @@ internal class ExtensionInstaller(private val context: Context) {
* Cancels extension install and remove from download manager and installer.
*/
fun cancelInstall(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName) ?: return
downloadManager.remove(downloadId)
Installer.cancelInstallQueue(context, downloadId)
activeJobs.remove(pkgName)?.cancel()
Installer.cancelInstallQueue(context, pkgName.hashCode().toLong())
}
/**
@@ -230,91 +168,11 @@ internal class ExtensionInstaller(private val context: Context) {
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsStateFlows[downloadId]?.let { it.value = step }
}
/**
* Deletes the download for the given package name.
*
* @param pkgName The package name of the download to delete.
*/
private fun deleteDownload(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
downloadsStateFlows.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
}
}
/**
* Receiver that listens to download status events.
*/
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
/**
* Whether this receiver is currently registered.
*/
private var isRegistered = false
/**
* Registers this receiver if it's not already.
*/
fun register() {
if (isRegistered) return
isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**
* Unregisters this receiver if it's not already.
*/
fun unregister() {
if (!isRegistered) return
isRegistered = false
context.unregisterReceiver(this)
}
/**
* Called when a download event is received. It looks for the download in the current active
* downloads and notifies its installation step.
*/
override fun onReceive(context: Context, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
// Avoid events for downloads we didn't request
if (id !in activeDownloads.values) return
val uri = downloadManager.getUriForDownloadedFile(id)
// Set next installation step
if (uri == null) {
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
updateInstallStep(id, InstallStep.Error)
return
}
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))
}
}
}
activeSteps[downloadId]?.let { it.value = step }
}
companion object {
const val APK_MIME = "application/vnd.android.package-archive"
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
const val FILE_SCHEME = "file://"
}
}
@@ -50,79 +50,46 @@ class ExtensionsScreenModel(
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
}
}
val queryFilter: (String) -> ((Extension) -> Boolean) = { query ->
filter@{ extension ->
if (query.isEmpty()) return@filter true
query.split(",").any { _input ->
val input = _input.trim()
if (input.isEmpty()) return@any false
when (extension) {
is Extension.Available -> {
extension.sources.any {
it.name.contains(input, ignoreCase = true) ||
it.baseUrl.contains(input, ignoreCase = true) ||
it.id == input.toLongOrNull()
} ||
extension.name.contains(input, ignoreCase = true)
}
is Extension.Installed -> {
extension.sources.any {
it.name.contains(input, ignoreCase = true) ||
it.id == input.toLongOrNull() ||
if (it is HttpSource) {
it.baseUrl.contains(input, ignoreCase = true)
} else {
false
}
} ||
extension.name.contains(input, ignoreCase = true)
}
is Extension.Untrusted -> extension.name.contains(input, ignoreCase = true)
}
}
}
}
screenModelScope.launchIO {
combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
state.map { it.searchQuery }
.distinctUntilChanged()
.debounce(SEARCH_DEBOUNCE_MILLIS)
.map { searchQueryPredicate(it ?: "") },
currentDownloads,
getExtensions.subscribe(),
) { query, downloads, (_updates, _installed, _available, _untrusted) ->
val searchQuery = query ?: ""
val itemsGroups: ItemGroups = mutableMapOf()
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
if (updates.isNotEmpty()) {
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates
}
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads))
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = installed + untrusted
}
val languagesWithExtensions = _available
.filter(queryFilter(searchQuery))
.groupBy { it.lang }
.toSortedMap(LocaleHelper.comparator)
.map { (lang, exts) ->
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
exts.map(extensionMapper(downloads))
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
buildMap {
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
if (updates.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
}
if (languagesWithExtensions.isNotEmpty()) {
itemsGroups.putAll(languagesWithExtensions)
}
itemsGroups
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
}
val languagesWithExtensions = _available
.filter(predicate)
.groupBy { it.lang }
.toSortedMap(LocaleHelper.comparator)
.map { (lang, exts) ->
ExtensionUiModel.Header.Text(LocaleHelper.getSourceDisplayName(lang, context)) to
exts.map(extensionMapper(downloads))
}
if (languagesWithExtensions.isNotEmpty()) {
putAll(languagesWithExtensions)
}
}
}
.collectLatest {
.collectLatest { items ->
mutableState.update { state ->
state.copy(
isLoading = false,
items = it,
items = items,
)
}
}
@@ -139,6 +106,36 @@ class ExtensionsScreenModel(
.launchIn(screenModelScope)
}
fun searchQueryPredicate(query: String): (Extension) -> Boolean {
val subqueries = query.split(",")
.map { it.trim() }
.filterNot { it.isBlank() }
if (subqueries.isEmpty()) return { true }
return { extension ->
subqueries.any { subquery ->
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
when (extension) {
is Extension.Installed -> extension.sources.any { source ->
source.name.contains(subquery, ignoreCase = true) ||
(source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true ||
source.id == subquery.toLongOrNull()
}
is Extension.Available -> extension.sources.any {
it.name.contains(subquery, ignoreCase = true) ||
it.baseUrl.contains(subquery, ignoreCase = true) ||
it.id == subquery.toLongOrNull()
}
else -> false
}
}
}
}
fun search(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
@@ -222,7 +219,7 @@ class ExtensionsScreenModel(
}
}
typealias ItemGroups = MutableMap<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
typealias ItemGroups = Map<ExtensionUiModel.Header, List<ExtensionUiModel.Item>>
object ExtensionUiModel {
sealed interface Header {
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.extension
import androidx.activity.compose.BackHandler
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -49,6 +50,10 @@ fun extensionsTab(
),
),
content = { contentPadding, _ ->
BackHandler(enabled = state.searchQuery != null) {
extensionsScreenModel.search(null)
}
ExtensionScreen(
state = state,
contentPadding = contentPadding,
@@ -9,11 +9,14 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
@@ -29,7 +32,6 @@ import mihon.feature.migration.config.MigrationConfigScreen
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@@ -75,20 +77,22 @@ data class MigrateMangaScreen(
)
},
floatingActionButton = {
if (state.selectionMode) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
},
onClick = {
val selection = state.selection
screenModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
expanded = lazyListState.shouldExpandFAB(),
)
}
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = {
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
},
onClick = {
val selection = state.selection
screenModel.clearSelection()
navigator.push(MigrationConfigScreen(selection))
},
expanded = lazyListState.shouldExpandFAB(),
modifier = Modifier.animateFloatingActionButton(
visible = state.selectionMode,
alignment = Alignment.BottomEnd,
),
)
},
) { contentPadding ->
if (state.isEmpty) {
@@ -1,17 +1,20 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material3.Icon
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import cafe.adriel.voyager.core.model.rememberScreenModel
@@ -34,7 +37,6 @@ import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
@@ -73,13 +75,15 @@ data class MigrateSourceSearchScreen(
)
},
floatingActionButton = {
AnimatedVisibility(visible = state.filters.isNotEmpty()) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
onClick = screenModel::openFilterSheet,
)
}
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_filter)) },
icon = { Icon(Icons.Outlined.FilterList, contentDescription = null) },
onClick = screenModel::openFilterSheet,
modifier = Modifier.animateFloatingActionButton(
visible = state.filters.isNotEmpty(),
alignment = Alignment.BottomEnd,
),
)
},
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { paddingValues ->
@@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.ui.download
import android.view.LayoutInflater
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -16,8 +13,10 @@ import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -56,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.Pill
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@@ -201,39 +199,37 @@ object DownloadQueueScreen : Screen() {
)
},
floatingActionButton = {
AnimatedVisibility(
visible = downloadList.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
val isRunning by screenModel.isDownloaderRunning.collectAsState()
ExtendedFloatingActionButton(
text = {
val id = if (isRunning) {
MR.strings.action_pause
} else {
MR.strings.action_resume
}
Text(text = stringResource(id))
},
icon = {
val icon = if (isRunning) {
Icons.Outlined.Pause
} else {
Icons.Filled.PlayArrow
}
Icon(imageVector = icon, contentDescription = null)
},
onClick = {
if (isRunning) {
screenModel.pauseDownloads()
} else {
screenModel.startDownloads()
}
},
expanded = fabExpanded,
)
}
val isRunning by screenModel.isDownloaderRunning.collectAsState()
SmallExtendedFloatingActionButton(
text = {
val id = if (isRunning) {
MR.strings.action_pause
} else {
MR.strings.action_resume
}
Text(text = stringResource(id))
},
icon = {
val icon = if (isRunning) {
Icons.Outlined.Pause
} else {
Icons.Filled.PlayArrow
}
Icon(imageVector = icon, contentDescription = null)
},
onClick = {
if (isRunning) {
screenModel.pauseDownloads()
} else {
screenModel.startDownloads()
}
},
expanded = fabExpanded,
modifier = Modifier.animateFloatingActionButton(
visible = downloadList.isNotEmpty(),
alignment = Alignment.BottomEnd,
),
)
},
) { contentPadding ->
if (downloadList.isEmpty()) {
@@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.LocalSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
private const val LOCAL_SOURCE_ID_ALIAS = "local"
data class LibraryItem(
val libraryManga: LibraryManga,
val downloadCount: Long = -1,
@@ -23,9 +26,17 @@ data class LibraryItem(
* @return true if the manga matches the query, false otherwise.
*/
fun matches(constraint: String): Boolean {
val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() }
val source = sourceManager.getOrStub(libraryManga.manga.source)
val sourceName by lazy { source.getNameForMangaInfo() }
if (constraint.startsWith("id:", true)) {
return id == constraint.substringAfter("id:").toLongOrNull()
} else if (constraint.startsWith("src:", true)) {
val querySource = constraint.substringAfter("src:")
return if (querySource.equals(LOCAL_SOURCE_ID_ALIAS, ignoreCase = true)) {
source.id == LocalSource.ID
} else {
source.id == querySource.toLongOrNull()
}
}
return libraryManga.manga.title.contains(constraint, true) ||
(libraryManga.manga.author?.contains(constraint, true) ?: false) ||
@@ -47,6 +47,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.history.interactor.GetNextChapters
@@ -73,6 +74,7 @@ class LibraryScreenModel(
private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getBookmarkedChaptersByMangaId: GetBookmarkedChaptersByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
@@ -234,9 +236,7 @@ class LibraryScreenModel(
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
val mangaTracks = trackMap
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
.orEmpty()
val mangaTracks = trackMap[item.id].orEmpty().map { it.trackerId }
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
val isIncluded = includedTracks.isEmpty() || mangaTracks.fastAny { it in includedTracks }
@@ -467,15 +467,19 @@ class LibraryScreenModel(
* Queues the amount specified of unread chapters from the list of selected manga
*/
fun performDownloadAction(action: DownloadAction) {
val mangas = state.value.selectedManga
val amount = when (action) {
DownloadAction.NEXT_1_CHAPTER -> 1
DownloadAction.NEXT_5_CHAPTERS -> 5
DownloadAction.NEXT_10_CHAPTERS -> 10
DownloadAction.NEXT_25_CHAPTERS -> 25
DownloadAction.UNREAD_CHAPTERS -> null
when (action) {
DownloadAction.NEXT_1_CHAPTER -> downloadNextChapters(1)
DownloadAction.NEXT_5_CHAPTERS -> downloadNextChapters(5)
DownloadAction.NEXT_10_CHAPTERS -> downloadNextChapters(10)
DownloadAction.NEXT_25_CHAPTERS -> downloadNextChapters(25)
DownloadAction.UNREAD_CHAPTERS -> downloadNextChapters(null)
DownloadAction.BOOKMARKED_CHAPTERS -> downloadBookmarkedChapters()
}
clearSelection()
}
private fun downloadNextChapters(amount: Int?) {
val mangas = state.value.selectedManga
screenModelScope.launchNonCancellable {
mangas.forEach { manga ->
val chapters = getNextChapters.await(manga.id)
@@ -496,6 +500,26 @@ class LibraryScreenModel(
}
}
private fun downloadBookmarkedChapters() {
val mangas = state.value.selectedManga
screenModelScope.launchNonCancellable {
mangas.forEach { manga ->
val chapters = getBookmarkedChaptersByMangaId.await(manga.id)
.fastFilterNot { chapter ->
downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)
}
downloadManager.downloadChapters(manga, chapters)
}
}
}
/**
* Marks mangas' chapters read status.
*/
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.manga
import android.content.Context
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.systemBarsPadding
@@ -59,12 +58,10 @@ import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.feature.migration.config.MigrationConfigScreen
import mihon.feature.migration.dialog.MigrateMangaDialog
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.screens.LoadingScreen
class MangaScreen(
@@ -325,12 +322,7 @@ class MangaScreen(
try {
getMangaUrl(manga_, source_)?.let { url ->
val intent = url.toUri().toShareIntent(context, type = "text/plain")
context.startActivity(
Intent.createChooser(
intent,
context.stringResource(MR.strings.action_share),
),
)
context.startActivity(intent)
}
} catch (e: Exception) {
context.toast(e.message)
@@ -650,6 +650,13 @@ class MangaScreenModel(
return if (manga.sortDescending()) chaptersSorted.reversed() else chaptersSorted
}
private fun getBookmarkedChapters(): List<Chapter> {
val chapterItems = if (skipFiltered) filteredChapters.orEmpty() else allChapters.orEmpty()
return chapterItems
.filter { (chapter, dlStatus) -> chapter.bookmark && dlStatus == Download.State.NOT_DOWNLOADED }
.map { it.chapter }
}
private fun startDownload(
chapters: List<Chapter>,
startNow: Boolean,
@@ -712,6 +719,7 @@ class MangaScreenModel(
DownloadAction.NEXT_10_CHAPTERS -> getUnreadChaptersSorted().take(10)
DownloadAction.NEXT_25_CHAPTERS -> getUnreadChaptersSorted().take(25)
DownloadAction.UNREAD_CHAPTERS -> getUnreadChapters()
DownloadAction.BOOKMARKED_CHAPTERS -> getBookmarkedChapters()
}
if (chaptersToDownload.isNotEmpty()) {
startDownload(chaptersToDownload, false)
@@ -953,7 +961,6 @@ class MangaScreenModel(
fun toggleSelection(
item: ChapterList.Item,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
updateSuccessState { successState ->
@@ -968,7 +975,7 @@ class MangaScreenModel(
set(selectedIndex, selectedItem.copy(selected = selected))
selectedChapterIds.addOrRemove(item.id, selected)
if (selected && userSelected && fromLongPress) {
if (selected && fromLongPress) {
if (firstSelection) {
selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex
@@ -994,7 +1001,7 @@ class MangaScreenModel(
}
}
}
} else if (userSelected && !fromLongPress) {
} else if (!fromLongPress) {
if (!selected) {
if (selectedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected }
@@ -344,7 +344,9 @@ class ReaderActivity : BaseActivity() {
}
override fun onPause() {
viewModel.flushReadTimer()
lifecycleScope.launchNonCancellable {
viewModel.updateHistory()
}
super.onPause()
}
@@ -576,7 +578,7 @@ class ReaderActivity : BaseActivity() {
private fun shareChapter() {
assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain")
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
startActivity(intent)
}
}
@@ -725,7 +727,7 @@ class ReaderActivity : BaseActivity() {
context = applicationContext,
message = stringResource(MR.strings.share_page_info, manga.title, chapter.name, page.number),
)
startActivity(Intent.createChooser(intent, stringResource(MR.strings.action_share)))
startActivity(intent)
}
private fun onCopyImageResult(uri: Uri) {
@@ -777,7 +779,8 @@ class ReaderActivity : BaseActivity() {
* Updates viewer inset depending on fullscreen reader preferences.
*/
private fun updateViewerInset(fullscreen: Boolean, drawUnderCutout: Boolean) {
val view = viewModel.state.value.viewer?.getView() ?: return
if (!::binding.isInitialized) return
val view = binding.viewerContainer
view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen, drawUnderCutout)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
@@ -832,7 +835,7 @@ class ReaderActivity : BaseActivity() {
private val grayBackgroundColor = Color.rgb(0x20, 0x21, 0x25)
/**
/*
* Initializes the reader subscriptions.
*/
init {
@@ -346,7 +346,7 @@ class ReaderViewModel @JvmOverloads constructor(
viewModelScope.launchIO {
logcat { "Loading ${chapter.chapter.url}" }
flushReadTimer()
updateHistory()
restartReadTimer()
try {
@@ -501,7 +501,7 @@ class ReaderViewModel @JvmOverloads constructor(
* if setting is enabled and [currentChapter] is queued for download
*/
private fun cancelQueuedDownloads(currentChapter: ReaderChapter): Download? {
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!.toLong())?.also {
return downloadManager.getQueuedDownloadOrNull(currentChapter.chapter.id!!)?.also {
downloadManager.cancelQueuedDownloads(listOf(it))
}
}
@@ -585,26 +585,20 @@ class ReaderViewModel @JvmOverloads constructor(
chapterReadStartTime = Instant.now().toEpochMilli()
}
fun flushReadTimer() {
getCurrentChapter()?.let {
viewModelScope.launchNonCancellable {
updateHistory(it)
}
}
}
/**
* Saves the chapter last read history if incognito mode isn't on.
*/
private suspend fun updateHistory(readerChapter: ReaderChapter) {
if (incognitoMode) return
suspend fun updateHistory() {
getCurrentChapter()?.let { readerChapter ->
if (incognitoMode) return@let
val chapterId = readerChapter.chapter.id!!
val endTime = Date()
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
val chapterId = readerChapter.chapter.id!!
val endTime = Date()
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
chapterReadStartTime = null
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
chapterReadStartTime = null
}
}
/**
@@ -655,7 +649,7 @@ class ReaderViewModel @JvmOverloads constructor(
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
id = chapter.id!!.toLong(),
id = chapter.id!!,
bookmark = bookmarked,
),
)
@@ -51,15 +51,8 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
// Add previous chapter pages and transition.
if (chapters.prevChapter != null) {
// We only need to add the last few pages of the previous chapter, because it'll be
// selected as the current chapter when one of those pages is selected.
val prevPages = chapters.prevChapter.pages
if (prevPages != null) {
newItems.addAll(prevPages.takeLast(2))
}
}
// Add previous chapter pages and transition
chapters.prevChapter?.pages?.let(newItems::addAll)
// Skip transition page if the chapter is loaded & current page is not a transition page
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
@@ -101,14 +94,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
}
}
if (chapters.nextChapter != null) {
// Add at most two pages, because this chapter will be selected before the user can
// swap more pages.
val nextPages = chapters.nextChapter.pages
if (nextPages != null) {
newItems.addAll(nextPages.take(2))
}
}
chapters.nextChapter?.pages?.let(newItems::addAll)
// Resets double-page splits, else insert pages get misplaced
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
@@ -123,7 +109,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
// Will skip insert page otherwise
if (insertPageLastPage != null) {
viewer.moveToPage(insertPageLastPage!!)
viewer.moveToPage(insertPageLastPage)
}
}
@@ -194,7 +180,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
}
fun cleanupPageSplit() {
val insertPages = items.filterIsInstance(InsertPage::class.java)
val insertPages = items.filterIsInstance<InsertPage>()
items.removeAll(insertPages)
notifyDataSetChanged()
}
@@ -43,14 +43,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
// Add previous chapter pages and transition.
if (chapters.prevChapter != null) {
// We only need to add the last few pages of the previous chapter, because it'll be
// selected as the current chapter when one of those pages is selected.
val prevPages = chapters.prevChapter.pages
if (prevPages != null) {
newItems.addAll(prevPages.takeLast(2))
}
}
chapters.prevChapter?.pages?.let(newItems::addAll)
// Skip transition page if the chapter is loaded & current page is not a transition page
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
@@ -70,14 +63,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
}
if (chapters.nextChapter != null) {
// Add at most two pages, because this chapter will be selected before the user can
// swap more pages.
val nextPages = chapters.nextChapter.pages
if (nextPages != null) {
newItems.addAll(nextPages.take(2))
}
}
chapters.nextChapter?.pages?.let(newItems::addAll)
updateItems(newItems)
}
@@ -103,6 +103,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
velocityX: Float,
velocityY: Float,
): Boolean {
recycler?.onManualScroll()
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
}
}
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
@@ -35,10 +34,6 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var firstVisibleItemPosition = 0
private var lastVisibleItemPosition = 0
private var currentScale = DEFAULT_RATE
private var isScrolling = false
private var hasTappedWhileScrolling = false
var zoomOutDisabled = false
set(value) {
field = value
@@ -57,6 +52,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
var tapListener: ((MotionEvent) -> Unit)? = null
var longTapListener: ((MotionEvent) -> Boolean)? = null
private var isManuallyScrolling = false
private var tapDuringManualScroll = false
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
halfWidth = MeasureSpec.getSize(widthSpec) / 2
halfHeight = MeasureSpec.getSize(heightSpec) / 2
@@ -67,11 +65,11 @@ class WebtoonRecyclerView @JvmOverloads constructor(
super.onMeasure(widthSpec, heightSpec)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent): Boolean {
if (e.actionMasked == MotionEvent.ACTION_DOWN) {
hasTappedWhileScrolling = isScrolling
tapDuringManualScroll = isManuallyScrolling
}
detector.onTouchEvent(e)
return super.onTouchEvent(e)
}
@@ -91,7 +89,10 @@ class WebtoonRecyclerView @JvmOverloads constructor(
val totalItemCount = layoutManager?.itemCount ?: 0
atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
atFirstPosition = firstVisibleItemPosition == 0
isScrolling = state != SCROLL_STATE_IDLE
if (state == SCROLL_STATE_IDLE) {
isManuallyScrolling = false
}
}
private fun getPositionX(positionX: Float): Float {
@@ -222,10 +223,14 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
}
fun onManualScroll() {
isManuallyScrolling = true
}
inner class GestureListener : GestureDetectorWithLongTap.Listener() {
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
if (!hasTappedWhileScrolling) {
if (!tapDuringManualScroll) {
tapListener?.invoke(ev)
}
return false
@@ -14,7 +14,7 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
internal val trackerManager: TrackerManager by injectLazy()
abstract fun handleResult(data: Uri?)
abstract fun handleResult(uri: Uri)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -23,7 +23,12 @@ abstract class BaseOAuthLoginActivity : BaseActivity() {
LoadingScreen()
}
handleResult(intent.data)
val data = intent.data
if (data == null) {
returnToSettings()
} else {
handleResult(data)
}
}
internal fun returnToSettings() {
@@ -2,69 +2,64 @@ package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.common.util.lang.launchIO
import kotlinx.coroutines.launch
class TrackLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
when (data?.host) {
"anilist-auth" -> handleAnilist(data)
"bangumi-auth" -> handleBangumi(data)
"myanimelist-auth" -> handleMyAnimeList(data)
"shikimori-auth" -> handleShikimori(data)
override fun handleResult(uri: Uri) {
val data = when {
!uri.encodedQuery.isNullOrBlank() -> uri.encodedQuery
!uri.encodedFragment.isNullOrBlank() -> uri.encodedFragment
else -> null
}
?.split("&")
?.filter { it.isNotBlank() }
?.associate {
val parts = it.split("=", limit = 2).map<String, String>(Uri::decode)
parts[0] to parts.getOrNull(1)
}
.orEmpty()
lifecycleScope.launch {
when (uri.host) {
"anilist-auth" -> handleAniList(data["access_token"])
"bangumi-auth" -> handleBangumi(data["code"])
"myanimelist-auth" -> handleMyAnimeList(data["code"])
"shikimori-auth" -> handleShikimori(data["code"])
}
returnToSettings()
}
}
private fun handleAnilist(data: Uri) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
lifecycleScope.launchIO {
trackerManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings()
}
private suspend fun handleAniList(accessToken: String?) {
if (accessToken != null) {
trackerManager.aniList.login(accessToken)
} else {
trackerManager.aniList.logout()
returnToSettings()
}
}
private fun handleBangumi(data: Uri) {
val code = data.getQueryParameter("code")
private suspend fun handleBangumi(code: String?) {
if (code != null) {
lifecycleScope.launchIO {
trackerManager.bangumi.login(code)
returnToSettings()
}
trackerManager.bangumi.login(code)
} else {
trackerManager.bangumi.logout()
returnToSettings()
}
}
private fun handleMyAnimeList(data: Uri) {
val code = data.getQueryParameter("code")
private suspend fun handleMyAnimeList(code: String?) {
if (code != null) {
lifecycleScope.launchIO {
trackerManager.myAnimeList.login(code)
returnToSettings()
}
trackerManager.myAnimeList.login(code)
} else {
trackerManager.myAnimeList.logout()
returnToSettings()
}
}
private fun handleShikimori(data: Uri) {
val code = data.getQueryParameter("code")
private suspend fun handleShikimori(code: String?) {
if (code != null) {
lifecycleScope.launchIO {
trackerManager.shikimori.login(code)
returnToSettings()
}
trackerManager.shikimori.login(code)
} else {
trackerManager.shikimori.logout()
returnToSettings()
}
}
}
@@ -4,6 +4,7 @@ import android.app.Application
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.ui.util.fastFilter
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
@@ -27,11 +28,16 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.system.logcat
@@ -40,9 +46,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.ZonedDateTime
@@ -57,6 +65,7 @@ class UpdatesScreenModel(
private val getManga: GetManga = Injekt.get(),
private val getChapter: GetChapter = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
private val updatesPreferences: UpdatesPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
@@ -75,19 +84,35 @@ class UpdatesScreenModel(
val limit = ZonedDateTime.now().minusMonths(3).toInstant()
combine(
getUpdates.subscribe(limit).distinctUntilChanged(),
// needed for SQL filters (unread, started, bookmarked, etc)
getUpdatesItemPreferenceFlow()
.distinctUntilChanged()
.flatMapLatest {
getUpdates.subscribe(
limit,
unread = it.filterUnread.toBooleanOrNull(),
started = it.filterStarted.toBooleanOrNull(),
bookmarked = it.filterBookmarked.toBooleanOrNull(),
hideExcludedScanlators = it.filterExcludedScanlators,
).distinctUntilChanged()
},
downloadCache.changes,
downloadManager.queueState,
) { updates, _, _ -> updates }
.catch {
logcat(LogPriority.ERROR, it)
_events.send(Event.InternalError)
}
.collectLatest { updates ->
// needed for Kotlin filters (downloaded)
getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new ->
old.filterDownloaded == new.filterDownloaded
},
) { updates, _, _, itemPreferences ->
updates
.toUpdateItems()
.applyFilters(itemPreferences)
.toPersistentList()
}
.collectLatest { updateItems ->
mutableState.update {
it.copy(
isLoading = false,
items = updates.toUpdateItems(),
items = updateItems,
)
}
}
@@ -98,9 +123,43 @@ class UpdatesScreenModel(
.catch { logcat(LogPriority.ERROR, it) }
.collect(this@UpdatesScreenModel::updateDownloadState)
}
getUpdatesItemPreferenceFlow()
.map { prefs ->
listOf(
prefs.filterUnread,
prefs.filterDownloaded,
prefs.filterStarted,
prefs.filterBookmarked,
)
.any { it != TriState.DISABLED }
}
.distinctUntilChanged()
.onEach {
mutableState.update { state ->
state.copy(hasActiveFilters = it)
}
}
.launchIn(screenModelScope)
}
private fun List<UpdatesWithRelations>.toUpdateItems(): PersistentList<UpdatesItem> {
private fun List<UpdatesItem>.applyFilters(
preferences: ItemPreferences,
): List<UpdatesItem> {
val filterDownloaded = preferences.filterDownloaded
val filterFnDownloaded: (UpdatesItem) -> Boolean = {
applyFilter(filterDownloaded) {
it.downloadStateProvider() == Download.State.DOWNLOADED
}
}
return fastFilter {
filterFnDownloaded(it)
}
}
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
return this
.map { update ->
val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId)
@@ -123,7 +182,6 @@ class UpdatesScreenModel(
selected = update.chapterId in selectedChapterIds,
)
}
.toPersistentList()
}
fun updateLibrary(): Boolean {
@@ -267,7 +325,6 @@ class UpdatesScreenModel(
fun toggleSelection(
item: UpdatesItem,
selected: Boolean,
userSelected: Boolean = false,
fromLongPress: Boolean = false,
) {
mutableState.update { state ->
@@ -282,7 +339,7 @@ class UpdatesScreenModel(
set(selectedIndex, selectedItem.copy(selected = selected))
selectedChapterIds.addOrRemove(item.update.chapterId, selected)
if (selected && userSelected && fromLongPress) {
if (selected && fromLongPress) {
if (firstSelection) {
selectedPositions[0] = selectedIndex
selectedPositions[1] = selectedIndex
@@ -308,7 +365,7 @@ class UpdatesScreenModel(
}
}
}
} else if (userSelected && !fromLongPress) {
} else if (!fromLongPress) {
if (!selected) {
if (selectedIndex == selectedPositions[0]) {
selectedPositions[0] = indexOfFirst { it.selected }
@@ -361,9 +418,41 @@ class UpdatesScreenModel(
libraryPreferences.newUpdatesCount().set(0)
}
private fun getUpdatesItemPreferenceFlow(): Flow<ItemPreferences> {
return combine(
updatesPreferences.filterDownloaded().changes(),
updatesPreferences.filterUnread().changes(),
updatesPreferences.filterStarted().changes(),
updatesPreferences.filterBookmarked().changes(),
updatesPreferences.filterExcludedScanlators().changes(),
) { downloaded, unread, started, bookmarked, excludedScanlators ->
ItemPreferences(
filterDownloaded = downloaded,
filterUnread = unread,
filterStarted = started,
filterBookmarked = bookmarked,
filterExcludedScanlators = excludedScanlators,
)
}
}
fun showFilterDialog() {
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
}
@Immutable
private data class ItemPreferences(
val filterDownloaded: TriState,
val filterUnread: TriState,
val filterStarted: TriState,
val filterBookmarked: TriState,
val filterExcludedScanlators: Boolean,
)
@Immutable
data class State(
val isLoading: Boolean = true,
val hasActiveFilters: Boolean = false,
val items: PersistentList<UpdatesItem> = persistentListOf(),
val dialog: Dialog? = null,
) {
@@ -387,6 +476,7 @@ class UpdatesScreenModel(
sealed interface Dialog {
data class DeleteConfirmation(val toDelete: List<UpdatesItem>) : Dialog
data object FilterSheet : Dialog
}
sealed interface Event {
@@ -395,6 +485,14 @@ class UpdatesScreenModel(
}
}
private fun TriState.toBooleanOrNull(): Boolean? {
return when (this) {
TriState.DISABLED -> null
TriState.ENABLED_IS -> true
TriState.ENABLED_NOT -> false
}
}
@Immutable
data class UpdatesItem(
val update: UpdatesWithRelations,
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.ui.updates
import cafe.adriel.voyager.core.model.ScreenModel
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.domain.updates.service.UpdatesPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class UpdatesSettingsScreenModel(
val updatesPreferences: UpdatesPreferences = Injekt.get(),
) : ScreenModel {
fun toggleFilter(preference: (UpdatesPreferences) -> Preference<TriState>) {
preference(updatesPreferences).getAndSet {
it.next()
}
}
}
@@ -17,6 +17,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.updates.UpdateScreen
import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog
import eu.kanade.presentation.updates.UpdatesFilterDialog
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.download.DownloadQueueScreen
@@ -54,6 +55,7 @@ data object UpdatesTab : Tab {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { UpdatesScreenModel() }
val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() }
val state by screenModel.state.collectAsState()
UpdateScreen(
@@ -74,6 +76,8 @@ data object UpdatesTab : Tab {
context.startActivity(intent)
},
onCalendarClicked = { navigator.push(UpcomingScreen()) },
onFilterClicked = screenModel::showFilterDialog,
hasActiveFilters = state.hasActiveFilters,
)
val onDismissDialog = { screenModel.setDialog(null) }
@@ -84,6 +88,12 @@ data object UpdatesTab : Tab {
onConfirm = { screenModel.deleteChapters(dialog.toDelete) },
)
}
is UpdatesScreenModel.Dialog.FilterSheet -> {
UpdatesFilterDialog(
onDismissRequest = onDismissDialog,
screenModel = settingsScreenModel,
)
}
null -> {}
}
@@ -5,6 +5,7 @@ package androidx.preference
/**
* Returns package-private [EditTextPreference.getOnBindEditTextListener]
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
fun EditTextPreference.getOnBindEditTextListener(): EditTextPreference.OnBindEditTextListener? {
return onBindEditTextListener
}
@@ -1,6 +1,5 @@
package eu.kanade.test
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.Tracker
@@ -20,8 +19,7 @@ data class DummyTracker(
override val supportsPrivateTracking: Boolean = false,
override val isLoggedIn: Boolean = false,
override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
val valLogoColor: Int = Color.rgb(18, 25, 35),
val valLogo: Int = R.drawable.ic_tracker_anilist,
val valLogo: Int = R.drawable.brand_anilist,
val valStatuses: List<Long> = (1L..6L).toList(),
val valReadingStatus: Long = 1L,
val valRereadingStatus: Long = 1L,
@@ -34,8 +32,6 @@ data class DummyTracker(
override val client: OkHttpClient
get() = TODO("Not yet implemented")
override fun getLogoColor(): Int = valLogoColor
override fun getLogo(): Int = valLogo
override fun getStatusList(): List<Long> = valStatuses
@@ -75,6 +75,12 @@ class ShellInterface : IShellInterface.Stub() {
.invoke(pmInterface)
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
val installFlags = this::class.java.getField("installFlags")
installFlags.set(
this,
installFlags.getInt(this) or REPLACE_EXISTING_INSTALL_FLAG,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
setPackageSource(PackageInstaller.PACKAGE_SOURCE_STORE)
}
@@ -105,26 +111,27 @@ class ShellInterface : IShellInterface.Stub() {
.getMethod("openSession", Int::class.java)
.invoke(packageInstaller, sessionId)
(
session::class.java.getMethod(
"openWrite",
String::class.java,
Long::class.java,
Long::class.java,
).invoke(session, "extension", 0L, apk.length) as ParcelFileDescriptor
).let { fd ->
val revocable = Class.forName("android.os.SystemProperties")
.getMethod("getBoolean", String::class.java, Boolean::class.java)
.invoke(null, "fw.revocable_fd", false) as Boolean
session::class.java.getMethod(
"openWrite",
String::class.java,
Long::class.java,
Long::class.java,
)
.invoke(session, "extension", 0L, apk.length)
.let { it as ParcelFileDescriptor }
.let { fd ->
val revocable = Class.forName("android.os.SystemProperties")
.getMethod("getBoolean", String::class.java, Boolean::class.java)
.invoke(null, "fw.revocable_fd", false) as Boolean
if (revocable) {
ParcelFileDescriptor.AutoCloseOutputStream(fd)
} else {
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
.getConstructor(ParcelFileDescriptor::class.java)
.newInstance(fd) as OutputStream
if (revocable) {
ParcelFileDescriptor.AutoCloseOutputStream(fd)
} else {
Class.forName($$"android.os.FileBridge$FileBridgeOutputStream")
.getConstructor(ParcelFileDescriptor::class.java)
.newInstance(fd) as OutputStream
}
}
}
.use { output ->
apk.createInputStream().use { input -> input.copyTo(output) }
}
@@ -174,3 +181,7 @@ class ShellInterface : IShellInterface.Stub() {
return shellContext.createPackageContext("com.android.shell", 0)
}
}
// Constant hidden from the SDK
// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/core/java/android/content/pm/PackageManager.java;l=1682-1689
private const val REPLACE_EXISTING_INSTALL_FLAG = 0x00000002
@@ -20,6 +20,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -61,7 +62,6 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.Pill
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@@ -143,7 +143,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
SmallExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.migrationConfigScreen_continueButtonText)) },
icon = { Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null) },
onClick = {
@@ -330,13 +330,13 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
}
}
private fun updateSources(save: Boolean = true, action: (List<MigrationSource>) -> List<MigrationSource>) {
private fun updateSources(action: (List<MigrationSource>) -> List<MigrationSource>) {
mutableState.update { state ->
val updatedSources = action(state.sources)
val includedSources = updatedSources.mapNotNull { if (!it.isSelected) null else it.id }
state.copy(sources = updatedSources.sortedWith(sourcesComparator(includedSources)))
}
if (save) saveSources()
saveSources()
}
private fun initSources() {
@@ -368,7 +368,9 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : Screen() {
}
.toList()
updateSources(save = false) { sources }
mutableState.update { state ->
state.copy(sources = sources.sortedWith(sourcesComparator(includedSources)))
}
}
fun toggleSelection(id: Long) {
@@ -145,7 +145,7 @@ private class MigrateDialogScreenModel(
}
val selectedFlags = sourcePreference.migrationFlags().get()
mutableState.update {
it.copy(
State(
current = current,
target = target,
applicableFlags = applicableFlags,
@@ -41,8 +41,8 @@ abstract class BaseSmartSearchEngine<T>(
val eligibleManga = supervisorScope {
queries.map { query ->
async(Dispatchers.Default) {
val builtQuery = if (extraSearchParams != null) {
"$query ${extraSearchParams.trim()}"
val builtQuery = if (!extraSearchParams.isNullOrBlank()) {
"$query $extraSearchParams"
} else {
query
}
@@ -1,7 +1,6 @@
package mihon.feature.migration.list.search
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import mihon.domain.manga.model.toDomainManga
import tachiyomi.domain.manga.model.Manga
@@ -23,6 +22,6 @@ class SmartSourceSearchEngine(extraSearchParams: String?) : BaseSmartSearchEngin
}
private fun makeSearchAction(source: CatalogueSource): SearchAction<SManga> = { query ->
source.getSearchManga(1, query, FilterList()).mangas
source.getSearchManga(1, query, source.getFilterList()).mangas
}
}
@@ -54,9 +54,11 @@ fun CalenderHeader(
}
Row {
IconButton(onClick = onPreviousClick) {
@Suppress("DEPRECATION")
Icon(Icons.Default.KeyboardArrowLeft, stringResource(MR.strings.upcoming_calendar_prev))
}
IconButton(onClick = onNextClick) {
@Suppress("DEPRECATION")
Icon(Icons.Default.KeyboardArrowRight, stringResource(MR.strings.upcoming_calendar_next))
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M0,0h192v192H0z"
android:strokeLineJoin="round"
android:fillColor="#1f2631"
android:fillType="evenOdd"/>
<path
android:pathData="m63.6,43.6 l-36.8,105h28.6l6.2,-18.2h31.2l6.1,18.1h28.5L90.7,43.6zM68.2,107.1 L77.1,78.1 86.9,107.1z"
android:strokeLineJoin="round"
android:fillColor="#fefefe"
android:fillType="evenOdd"/>
<path
android:pathData="M127,148.5h32.5q6.3,0 6.4,-6.3v-14q-0.1,-6.4 -6.4,-6.5h-37.6V50q-0.1,-6.3 -6.4,-6.4h-14q-6.3,0.1 -6.4,6.4v7.8c-3.2,-9.8 31.8,90.7 32,90.7"
android:strokeLineJoin="round"
android:fillColor="#02a9ff"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,53 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group>
<clip-path
android:pathData="M0.03,0.03l192.01,0l0,192.01L0.03,192.03z"/>
<path
android:pathData="M0.03,0.03l191.95,0l0,191.95L0.03,191.97z"
android:strokeLineJoin="round"
android:fillColor="#2d2e2f"
android:fillType="evenOdd"/>
<path
android:pathData="M129.89,-36.09a3.42,3.42 0,1 1,5.7 3.87L87.4,36.59A31.9,31.9 0,0 1,100.22 46.16L157.23,46.16a17.09,17.09 0,0 1,17.09 17.09l0,93.98a17.09,17.09 0,0 1,-17.09 17.09L67.98,174.32L43.88,192.54a5.7,5.7 0,0 1,-8.54 -7.01l5.35,-11.22L-6.81,174.32a25.06,25.06 0,0 1,-25.06 -25.06L-31.87,63.25a17.09,17.09 0,0 1,17.09 -17.09L50.66,46.16a31.9,31.9 0,0 1,10.77 -8.71L-6.13,-31.76a3.42,3.42 0,0 1,4.9 -4.78L68.49,34.94a31.9,31.9 0,0 1,11.96 -0.34zM40.47,187.99l18.06,-13.67L46.96,174.32zM-6.81,55.28a7.97,7.97 0,0 0,-7.97 7.97l0,86.01a7.97,7.97 0,0 0,7.97 7.97l164.04,0a7.97,7.97 0,0 0,7.97 -7.97L165.2,63.25a7.97,7.97 0,0 0,-7.97 -7.97z"
android:strokeLineJoin="round"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="71.22"
android:startY="57.21"
android:endX="71.22"
android:endY="154.89"
android:type="linear">
<item android:offset="0" android:color="#FFFF3599"/>
<item android:offset="1" android:color="#FFFF77AB"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M157.23,55.28a7.97,7.97 0,0 1,7.97 7.97l0,86.01a7.97,7.97 0,0 1,-7.97 7.97L81.08,157.23L40.47,187.99l14.7,-30.76L-6.81,157.23a7.97,7.97 0,0 1,-7.97 -7.97L-14.78,63.25a7.97,7.97 0,0 1,7.97 -7.97z"
android:strokeLineJoin="round"
android:fillColor="#fff"
android:fillType="evenOdd"/>
<path
android:pathData="M93.44,92.3a2.28,2.28 0,0 1,1.94 3.42l-18.23,30.76a2.28,2.28 0,0 1,-3.87 0l-18.23,-30.76a2.28,2.28 0,0 1,1.94 -3.42zM145.84,119.07a2.28,2.28 0,0 1,0 4.56l-36.17,0a2.28,2.28 0,0 1,0 -4.56zM40.75,118.78a2.28,2.28 0,0 1,0 4.56L4.58,123.34a2.28,2.28 0,0 1,0 -4.56zM75.21,120.89 L89.45,96.85L60.97,96.85zM145.84,111.09a2.28,2.28 0,0 1,0 4.56l-36.17,0a2.28,2.28 0,0 1,0 -4.56zM40.75,110.81a2.28,2.28 0,0 1,0 4.56L4.58,115.37a2.28,2.28 0,0 1,0 -4.56zM144.98,78.51a2.28,2.28 0,1 1,1.71 4.21L118.9,94.01l27.8,11.28a2.28,2.28 0,1 1,-1.71 4.21l-33.04,-13.39a2.28,2.28 0,0 1,0 -4.21zM2.48,79.48a2.28,2.28 0,0 1,2.96 -1.25l33.04,13.39a2.28,2.28 0,0 1,0 4.21l-33.04,13.39a2.28,2.28 0,0 1,-1.71 -4.21l27.85,-11.28 -27.85,-11.28a2.28,2.28 0,0 1,-1.25 -2.96"
android:strokeLineJoin="round"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="75.21"
android:startY="78.17"
android:endX="75.21"
android:endY="129.75"
android:type="linear">
<item android:offset="0" android:color="#FFFF3599"/>
<item android:offset="1" android:color="#FFFF77AB"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>
@@ -0,0 +1,51 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M0,0h192v192H0z"
android:strokeLineJoin="round"
android:fillColor="#49c593"
android:fillType="nonZero"/>
<path
android:pathData="M46.5,43.8c44.3,0 44.3,0 48,3.1 0.8,1 0.8,1 0.8,2.4h1.4l0.5,-1.2c0.9,-1.6 0.9,-1.6 4.3,-4.4h44v90.8l-30,0.2h-7.2l-6.8,0.1c-2.5,0.5 -3.2,1.3 -4.8,3.1h-1.4l-1.2,-1.3c-2,-1.9 -3.3,-1.8 -6,-1.8h-8.6l-10.1,-0.2 -22.9,-0.1z"
android:strokeLineJoin="round"
android:fillColor="#fefefe"
android:fillType="nonZero"/>
<path
android:pathData="M39.6,54.8h6.9v79.7l30,-0.2h14c2.3,0.2 3.2,0.7 4.8,2.3v1.3h1.4l0.6,-1.3c0.8,-1.4 0.8,-1.4 2.1,-2.1h46.1V54.8h6.9c1.7,1.7 1.6,2.3 1.6,4.8v80.3c-0.2,1.5 -0.2,1.5 -1.6,2.8 -1.8,0.3 -1.8,0.3 -4.2,0.3h-37.8c-3.3,0.6 -5,2.6 -7.2,5 -2.5,2.4 -4.3,3 -7.8,3.3 -3.3,0 -5.3,-1.5 -7.7,-3.7L85,144c-2,-1 -3.4,-0.7 -5.5,-0.8h-4.1l-17,-0.2 -14.6,-0.1h-2.3c-1.9,-0.2 -1.9,-0.2 -3.2,-1.6 -0.3,-1.5 -0.3,-1.5 -0.3,-3.5V57.7c0.3,-1.6 0.3,-1.6 1.6,-3"
android:strokeLineJoin="round"
android:fillColor="#424d72"
android:fillType="nonZero"/>
<path
android:pathData="M46.5,43.8h43.3v0.6H47.2V129l29.6,-0.2h13.7c2.3,0.2 3.2,0.7 4.8,2.3v1.3h1.4l0.6,-1.3c0.8,-1.4 0.8,-1.4 2.1,-2.1h45.3V44.4h-43.3v-0.6h44v90.7l-30,0.2h-7.2l-6.8,0.1c-2.4,0.5 -3.1,1.3 -4.8,3.1h-1.4l-1.1,-1.3c-2,-1.9 -3.4,-1.8 -6.1,-1.8h-8.6l-10,-0.2 -22.9,-0.1z"
android:strokeLineJoin="round"
android:fillColor="#e2e6ec"
android:fillType="nonZero"/>
<path
android:pathData="M57.5,52h11v19.3h-11z"
android:strokeLineJoin="round"
android:fillColor="#59d1f7"
android:fillType="nonZero"/>
<path
android:pathData="M103.6,119.4h31.6v2.7h-31.6zM56.8,119.4h31.6v2.7L56.8,122.1zM103.6,111.1h31.6v2.8h-31.6zM56.8,111.1h31.6v2.8L56.8,113.9zM103.6,102.9h31.6v2.7h-31.6zM56.8,102.9h31.6v2.7L56.8,105.6zM103.6,94.6h31.6v2.8h-31.6zM56.8,94.6h31.6v2.8L56.8,97.4zM103.6,86.4h31.6L135.2,89h-31.6zM56.8,86.4h31.6L88.4,89L56.8,89zM103.6,78.1h31.6L135.2,81h-31.6zM56.8,78.1h31.6L88.4,81L56.8,81zM103.6,69.9h31.6v2.7h-31.6z"
android:strokeLineJoin="round"
android:fillColor="#e5e8f0"
android:fillType="nonZero"/>
<path
android:pathData="M103.6,61.6h31.6v2.8h-31.6zM103.6,53.4h31.6L135.2,56h-31.6z"
android:strokeLineJoin="round"
android:fillColor="#e4e7ef"
android:fillType="nonZero"/>
<path
android:pathData="M74.7,69.9h13.7v2.7H74.7z"
android:strokeLineJoin="round"
android:fillColor="#e5e8f0"
android:fillType="nonZero"/>
<path
android:pathData="M74.7,61.6h13.7v2.8L74.7,64.4zM74.7,53.4h13.7L88.4,56L74.7,56z"
android:strokeLineJoin="round"
android:fillColor="#e5e8ef"
android:fillType="nonZero"/>
</vector>
+21
View File
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M0,0h192v192H0z"
android:strokeLineJoin="round"
android:fillColor="#312631"
android:fillType="evenOdd"/>
<path
android:pathData="m113,51.3 l-2.2,0.2 -4.2,0.5 -1.5,0.2a59,59 0,0 0,-11.3 4L92,57v12.5l-0.2,13.7a10,10 0,0 1,-5.4 7.1,6 6,0 0,1 -3.5,0.8L80,91a189,189 0,0 1,-14.5 -5.5l-4.8,-1.9 -5.5,-1.8c-0.2,0 -1,0.8 -1.8,1.5l-2,1.7a199,199 0,0 0,-19 19.5,5 5,0 0,0 -1.5,4.3c0,0.8 1.3,2.5 2.2,2.9 1,0.5 2.4,0.6 3.3,0.3l2.5,-1.5a117,117 0,0 1,21.4 -11.9l2.2,-1 3,-1c2,0 4,1.5 4.4,3.2l0.2,0.8c0.3,0.3 -1.5,4.2 -2.2,4.9 -0.2,0.1 -0.3,0.4 -1.3,2.2l-0.7,1a64,64 0,0 0,-3.7 7.2l-0.8,1.7 -0.4,0.8 -0.9,2a98,98 0,0 0,-3.3 8.3c-1.4,4 -1.4,3.8 -1.4,5.2q0,2 1.6,3.3 0.8,0.8 2.3,0.8c1.9,0 2.8,-0.4 4.1,-2.2l2.6,-3.4a64,64 0,0 1,5.5 -6.6c1.5,-1.9 8.7,-9 10.5,-10.6a109,109 0,0 1,22.5 -16A125,125 0,0 1,138 86.6c3,-0.6 3.6,-0.6 4.6,0.6 0.7,0.7 0.6,2.4 -0.1,3.2a8,8 0,0 1,-3.7 1.5,76 76,0 0,0 -25.1,12 42,42 0,0 0,-6.8 5A66,66 0,0 0,89.5 132a51,51 0,0 0,-2.8 9.9,47 47,0 0,0 2.7,24c0.7,1.8 1.2,2.3 2.4,3 1.9,0.9 4.2,0.1 5.5,-2 3.6,-5.8 7.4,-9 15.5,-13l3,-1.4 0.7,-0.4 0.6,-0.3 1.8,-0.7 2.4,-1c1.4,-0.6 8.6,-4.3 9.7,-5l3.2,-2 3.6,-2.4 2.1,-1.4c2.6,-1.9 8,-7 10,-9.3l1,-1.3a44,44 0,0 0,5.8 -9.2,44 44,0 0,0 4.8,-21c0,-5 0,-5.9 -0.8,-9.5a48,48 0,0 0,-7.8 -18,47 47,0 0,0 -26,-18.1l-1.4,-0.3 -3,-0.7a69,69 0,0 0,-9.5 -0.7m-17.5,83.2a28,28 0,0 0,16.9 11.7l1,0.2 1.1,0.2q1.7,0.2 0.2,0.7a89,89 0,0 0,-13.2 7.3,34 34,0 0,0 -7.2,7.4c-0.7,1 -0.7,0.9 -1.7,-3.2a54,54 0,0 1,-1 -15,42 42,0 0,1 3.1,-10.3z"
android:strokeLineJoin="round"
android:fillColor="#f75239"
android:fillType="nonZero"/>
<path
android:pathData="M72.3,22.7c-1.5,0.3 -2.4,1.2 -3.9,3.5L66.6,30a30,30 0,0 0,-2.2 14.7c0.1,0.9 0.1,1 -1.1,1.6a50,50 0,0 0,-6.2 4.1l-1.6,1.3q-0.3,0.2 -1.6,-0.5a31,31 0,0 0,-16.7 -2.8c-4,0.6 -4.7,0.9 -5.6,2q-1.1,1.1 -1,2.7c0,1 0.6,2.8 1.2,3.4l1,1.2 0.6,1 1.5,1.7c3.5,4 8.3,8.5 12.5,11.3 4,2.6 5.5,3.3 12,5.8l8.4,3.3 1.7,0.6 3.6,1.4 2,0.8c2,0.9 5.3,2 6.4,2.2q1.3,0.4 2.3,0a4,4 0,0 0,2.7 -3,613 613,0 0,0 -0.4,-34.6 43,43 0,0 0,-1.4 -6.2L83,37c-0.5,-1.3 -4,-8.2 -5.2,-10.4 -1.8,-3.2 -3.3,-4.2 -5.5,-3.9m1,7.2c3,5.8 4.4,8.6 4.8,9.7 0.4,1.3 0.4,1.4 0,1.5h-0.7l-1,0.3 -1.7,0.4 -1.3,0.3 -0.6,0.2q-3,1 -3,0.8a15,15 0,0 1,0 -4.3l0.2,-1.9c0.3,-2.1 2.4,-7.4 3,-7.4zM44,54c2.4,0.3 6.3,1.5 7,2q0,0.4 -1.2,2l-1.6,2 -1.4,2 -1,1.6 -1.1,-1a76,76 0,0 1,-7.6 -8.6h1.3a26,26 0,0 1,5.6 0"
android:strokeLineJoin="round"
android:fillColor="#f75239"
android:fillType="nonZero"/>
</vector>
+57
View File
@@ -0,0 +1,57 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M0,0h192v192H0z"
android:strokeLineJoin="round"
android:fillColor="#005ed3"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M-0.01,-0.01l191.98,0l0,191.98L-0.01,191.98z"/>
<path
android:pathData="M223.63,135.63a88,88 70.86,0 1,-99.89 87.18l-31.63,-31.63 0.89,-5.95 -28.19,-28.22 22.1,-21.38 -22.14,-21.38 31.63,-5.29 -4.26,-28.91 28.6,12.72 14.88,-25.58 22.17,22.34 21.31,-9.49 43.69,43.69q0.83,5.84 0.83,11.89"
android:strokeLineJoin="round"
android:fillColor="#00459f"
android:fillType="evenOdd"/>
<path
android:pathData="M215.63,127.63a88,88 49.79,0 1,-99.86 87.21l-31.63,-31.63 0.89,-5.98 -28.26,-28.22 22.14,-21.38 -22.14,-21.38 31.63,-5.29 -4.26,-28.91 28.6,12.72L127.63,59.22l22.17,22.34 21.31,-9.49 43.72,43.72q0.79,5.84 0.79,11.86"
android:strokeLineJoin="round"
android:fillColor="#00459f"
android:fillType="evenOdd"/>
<path
android:pathData="M184,96a88,88 65.09,0 1,-99.86 87.21l-31.63,-31.63 0.89,-5.98 -28.26,-28.22 22.14,-21.38L25.19,74.62l31.63,-5.29 -4.26,-28.91 28.6,12.72L96,27.59l22.17,22.34 21.31,-9.49 43.72,43.72q0.79,5.84 0.79,11.86"
android:strokeLineJoin="round"
android:fillColor="#00459f"
android:fillType="evenOdd"/>
<path
android:pathData="m96,37.8 l12.75,21.93 24.27,-10.79 -3.61,24.65 26.47,4.43 -18.6,17.98 18.6,17.98 -26.47,4.47 3.61,24.61 -24.27,-10.79 -12.75,21.93 -12.75,-21.93 -24.27,10.79 3.61,-24.65 -26.47,-4.43 18.6,-17.98 -18.6,-17.98 26.47,-4.47 -3.61,-24.61 24.27,10.79z"
android:strokeLineJoin="round"
android:fillColor="#ff0335"
android:fillType="nonZero"/>
<path
android:pathData="m155.88,113.98 l-26.47,4.47 3.61,24.61 -24.27,-10.79 -12.75,21.93L96,37.8l12.75,21.93 24.27,-10.79 -2.34,15.81 -1.27,8.83 20.21,3.37 6.26,1.07 -4.61,4.47 -13.99,13.51 3.92,3.78z"
android:strokeLineJoin="round"
android:fillColor="#c2001b"
android:fillType="nonZero"/>
<path
android:pathData="m96,164.41 l-14.88,-25.54 -28.6,12.72 4.26,-28.91 -31.63,-5.29 22.14,-21.38L25.19,74.62l31.63,-5.29 -4.26,-28.91 28.6,12.72L96,27.59l14.88,25.58 28.6,-12.72 -4.26,28.91 31.63,5.29 -22.14,21.38 22.14,21.38 -31.63,5.29 4.26,28.91 -28.6,-12.72zM85.34,125.67 L96,143.92 106.66,125.7 126.56,134.53 123.57,114.18 144.95,110.57 129.86,96 144.98,81.43 123.57,77.82 126.56,57.47 106.62,66.33 96,48.08 85.34,66.3 65.44,57.47 68.43,77.82 47.05,81.43 62.14,96 47.02,110.57 68.43,114.18 65.44,134.53z"
android:strokeLineJoin="round"
android:fillColor="#ffdf47"
android:fillType="nonZero"/>
<path
android:pathData="m146.63,97.86 l-1.92,-1.86 8.63,-8.35 13.51,-13.03 -19.18,-3.2 -12.44,-2.06 0.96,-6.6 3.3,-22.34 -28.6,12.72L96,27.56l0,20.52l10.66,18.22 19.9,-8.83 -1.38,9.14 -0.86,5.98 -0.76,5.22 0.86,0.17 20.52,3.44 -15.09,14.58 5.84,5.64 9.28,8.94 -21.42,3.61 2.99,20.35 -19.94,-8.87 -10.62,18.25l0,20.52l14.88,-25.58 28.6,12.72 -4.26,-28.91 31.63,-5.29z"
android:strokeLineJoin="round"
android:fillColor="#fec000"
android:fillType="nonZero"/>
<path
android:pathData="m116.97,54.77 l-5.65,33.08 25.29,30.59l-25.1,7.46l-17.68,-24.55l7.88,27.46l-24.31,7.22L59.05,72.01l24.31,-7.22l7.13,24.87l3.67,-28.08z"
android:strokeLineJoin="round"
android:strokeWidth="1.6"
android:fillColor="#fff"
android:fillType="nonZero"
android:strokeColor="#000"/>
</group>
</vector>
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<path
android:pathData="M0,0h192v192H0z"
android:strokeLineJoin="round"
android:fillColor="#92a0ad"
android:fillType="evenOdd"/>
<path
android:pathData="M44.7,19h102.6A25.6,25.6 0,0 1,173 44.7v102.6a25.6,25.6 0,0 1,-25.7 25.7H44.7A25.6,25.6 0,0 1,19 147.3V44.7A25.6,25.6 0,0 1,44.7 19"
android:strokeLineJoin="round"
android:fillColor="#cbd6e8"
android:fillType="nonZero"/>
<path
android:pathData="M164.4,143.2q0,3.7 -0.5,7.5a22,22 0,0 1,-2.1 7q-1.5,3 -4.3,5.2a12,12 0,0 1,-7.4 2q-5.5,0 -8.5,-3.4a20,20 0,0 1,-3.8 -8.6,118 118,0 0,1 -2.3,-16.7l-1.6,-33.8a105,105 0,0 0,-2.8 -16.7,567 567,0 0,1 -8.5,26.3l-3.8,9.6a127,127 0,0 0,-5.2 17.8,175 175,0 0,1 -3,12.2q-0.7,2.2 -1.8,4.7 -0.8,2.4 -1.9,4.5a10,10 0,0 1,-2.2 3.1q-2.8,2.6 -6.7,3.3a26,26 0,0 1,-7 1,11 11,0 0,1 -9,-4 27,27 0,0 1,-4.9 -8.8,56 56,0 0,1 -1.9,-7.3l-1.5,-7.5q-2.3,-9.9 -5.4,-19.5 -3,-9.7 -5.6,-19.5 -3.5,7.8 -6.3,15.6a107,107 0,0 0,-4.7 28.6q0,6 -1.6,12.2 -1.2,4.5 -4,7 -2.6,2.6 -7.5,2.6a14,14 0,0 1,-12.2 -6.6q-1.7,-3 -2.4,-6.6 -0.5,-4 -0.5,-7.7 0,-8.2 2,-17.4 2.3,-9.5 5.1,-18.8l6.1,-18.7q3.4,-9.3 5.8,-17.2 2.3,-7.9 3.3,-15.5 1.2,-7.7 3,-15.5A26,26 0,0 1,55 31.4a21,21 0,0 1,5.6 -4q3,-1.5 6.8,-1.6 2.6,0 5.4,0.9a15,15 0,0 1,8.7 5.9q1.5,2.3 1.4,5.4 0,2.3 -0.4,4.5 -0.3,2.1 -0.3,4.4a271,271 0,0 0,10.6 74.9q7.2,-16.4 12,-33.5a1608,1608 0,0 0,12 -44.6q1.3,-5.1 3.7,-10 2.3,-4.5 5.6,-7.4 3.4,-3 8.7,-3a29,29 0,0 1,12.2 2.8q2.5,1.5 4.2,4 1.8,2.6 1.7,6.3 0,2.8 -0.3,5.6l-0.2,5.4a219,219 0,0 0,6.3 52q2.4,10.8 4,21.9 1.7,10.8 1.7,21.9"
android:strokeLineJoin="round"
android:fillColor="#ff8c15"
android:fillType="nonZero"/>
</vector>

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