Compare commits

..

486 Commits

Author SHA1 Message Date
Koitharu
34f6e5232b Update readme 2025-11-04 10:40:44 +02:00
Koitharu
f205c1b3dc Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2025-11-04 10:35:35 +02:00
Milo Ivir
4b2a487c37 Translated using Weblate (Croatian)
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
MuhamadSyabitHidayattulloh
726ac21974 Translated using Weblate (Indonesian)
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
Robert Broketa
6b35216949 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (896 of 896 strings)

Co-authored-by: Robert Broketa <robert@broketa.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
João Augusto Casagrande
22cae62f17 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: João Augusto Casagrande <joao.augusto1809@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
Oğuz Ersen
4733caf2e6 Translated using Weblate (Turkish)
Currently translated at 100.0% (896 of 896 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
Максим Горпиніч
d49103de1f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (896 of 896 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (894 of 894 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-11-04 10:34:14 +02:00
Koitharu
414bab7ce3 Update readme 2025-11-04 10:34:03 +02:00
Koitharu
64c1873eb5 Merge branch 'master' into devel 2025-11-04 09:49:07 +02:00
Koitharu
06a0b5829b Fix crashes
(cherry picked from commit 1d32f53bdd)
2025-11-03 20:27:53 +02:00
Koitharu
0ce2870c8b Fix chapters list not accessible
(cherry picked from commit 5701862661)
2025-11-03 20:27:24 +02:00
Koitharu
f59027666b Fix loading empty manga
(cherry picked from commit 5590ab7c8a)
2025-11-03 20:27:18 +02:00
Nathan Bapin
8513bc6daf Fix forget page when the screen is rotated (#1674)
(cherry picked from commit e2fcfcc7a8)
2025-11-03 20:27:10 +02:00
Koitharu
cceaefc896 Avoid memory leak in ExceptionResolver
(cherry picked from commit 7a3b2a9bb4)
2025-11-03 20:27:05 +02:00
Koitharu
1d32f53bdd Fix crashes 2025-11-03 20:26:15 +02:00
Koitharu
0e98dd8695 Refactor SearchMenuProvider 2025-11-03 20:23:43 +02:00
MuhamadSyabitHidayattulloh
119b7c2ac7 Add filtering options for pinned sources and empty results in search menu 2025-11-02 15:28:24 +02:00
Koitharu
5701862661 Fix chapters list not accessible 2025-11-02 15:26:56 +02:00
Koitharu
5590ab7c8a Fix loading empty manga 2025-11-02 15:12:32 +02:00
Koitharu
9fde0106be Fix code formatting 2025-11-02 11:05:14 +02:00
skepsun
e73f077dc5 remove unnecessary summary 2025-11-02 10:57:46 +02:00
skepsun
c37458d43a Add foldable device support (auto two-page) 2025-11-02 10:57:46 +02:00
Nathan Bapin
e2fcfcc7a8 Fix forget page when the screen is rotated (#1674) 2025-11-02 10:57:25 +02:00
Koitharu
7a3b2a9bb4 Avoid memory leak in ExceptionResolver 2025-11-02 10:56:23 +02:00
Koitharu
881f154b5e Update parsers 2025-11-02 09:47:46 +02:00
Koitharu
34be5d16f2 Merge pull request #1701 from weblate/weblate-kotatsu-strings 2025-11-02 09:22:10 +02:00
Milo Ivir
e7e554648d Translated using Weblate (Croatian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-10-30 04:25:59 +00:00
Draken
89a4180b46 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-10-30 04:25:55 +00:00
MuhamadSyabitHidayattulloh
4e2e190547 Translated using Weblate (Indonesian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-10-30 04:25:53 +00:00
João Augusto Casagrande
3c557aae6c Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: João Augusto Casagrande <joao.augusto1809@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-10-30 04:25:50 +00:00
Nicola Bortoletto
0b00a3675d Translated using Weblate (Italian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-10-30 04:25:41 +00:00
Alvoracz
8f20be6953 Translated using Weblate (Czech)
Currently translated at 97.8% (874 of 893 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-10-30 04:25:35 +00:00
Kanta Sekiguchi
26875c01c6 Translated using Weblate (Japanese)
Currently translated at 90.8% (811 of 893 strings)

Co-authored-by: Kanta Sekiguchi <kanta.sekiguchi360@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2025-10-30 04:25:32 +00:00
Koitharu
4beb34c1a5 Translated using Weblate (Russian)
Currently translated at 99.7% (891 of 893 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Conrado
1d50ab00c4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (889 of 889 strings)

Co-authored-by: Conrado <deadlocked53.89@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Ruffghanor
299cd229ec Translated using Weblate (Portuguese)
Currently translated at 100.0% (889 of 889 strings)

Co-authored-by: Ruffghanor <ruffghanor20@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Nahid hasan Limon
b02f394cd4 Translated using Weblate (Bengali)
Currently translated at 22.9% (204 of 889 strings)

Co-authored-by: Nahid hasan Limon <nahidhasanlimon401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Oğuz Ersen
7352f06564 Translated using Weblate (Turkish)
Currently translated at 100.0% (893 of 893 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Milo Ivir
1e4861367e Translated using Weblate (Croatian)
Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Draken
bc3208946b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
MuhamadSyabitHidayattulloh
d5fbb00676 Translated using Weblate (Indonesian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-26 17:51:58 +02:00
Hecker_01
7514362ca4 Translated using Weblate (Dutch)
Currently translated at 4.0% (36 of 880 strings)

Translated using Weblate (Dutch)

Currently translated at 88.8% (8 of 9 strings)

Added translation using Weblate (Dutch)

Added translation using Weblate (Dutch)

Co-authored-by: Hecker_01 <jesseflantua@icloud.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-26 17:51:58 +02:00
Infy's Tagalog Translations
e76a04bea0 Translated using Weblate (Filipino)
Currently translated at 98.9% (871 of 880 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Максим Горпиніч
732a6e7c26 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (882 of 882 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Макар Разин
f3111dc636 Translated using Weblate (Russian)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Nicola Bortoletto
e0e0cf4ecd Translated using Weblate (Italian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (882 of 882 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
Nataniel Dika Kurniawan
50f302a7f8 Translated using Weblate (Indonesian)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-10-26 17:51:58 +02:00
google-labs-jules[bot]
500995a9d8 feat(settings): Add "Every 6 hours" option for periodic backups
Adds a new "Every 6 hours" frequency option to the periodic backup settings.

To maintain consistency with the existing preference values, which are stored in days, this new option is represented internally as a fractional value of `0.25` days.

The implementation includes:
- Adding the new string resource and updating the preference arrays.
- Changing the preference type in `AppSettings.kt` from `Long` to `Float` to accommodate the fractional value.
- Updating the millisecond conversion logic to correctly calculate the interval from a float value in days.

This approach avoids a complex data migration and is simpler and safer than changing the base unit for all values from days to hours.
2025-10-26 17:36:51 +02:00
Koitharu
beaf5cc0d5 Remove SavedFilterBackup class 2025-10-26 17:35:29 +02:00
google-labs-jules[bot]
6377de470d feat: Add saved filters to backup and restore
This commit adds support for backing up and restoring saved filters.

- Added a new `SAVED_FILTERS` section to the backup process.
- Implemented the logic to read filters from SharedPreferences during backup and write them back during restore.
- Fixed compilation errors in `AppBackupAgent` and `BackupSectionModel`.
2025-10-26 17:30:49 +02:00
google-labs-jules[bot]
dec45f7851 feat: Add saved filters to backup and restore
This commit adds support for backing up and restoring saved filters.

- Added a new `SAVED_FILTERS` section to the backup process.
- Implemented the logic to read filters from SharedPreferences during backup and write them back during restore.
2025-10-26 17:30:49 +02:00
Koitharu
dbada34a43 Move pull gesture option to reader settings 2025-10-26 17:27:33 +02:00
Koitharu
b62467964e Fix filters on tablet 2025-10-26 17:03:37 +02:00
Koitharu
3249e10931 Exclude broken sources from catalog 2025-10-26 16:47:54 +02:00
Koitharu
0d5229b112 Improve local manga directories config screen 2025-10-26 16:33:59 +02:00
Koitharu
d0ed1fb85f Notify about broken source on list screen 2025-10-21 18:04:05 +03:00
Koitharu
9e5664da3a Reorganize settings 2025-10-21 17:51:11 +03:00
Koitharu
35c158d35a Update readme 2025-10-21 15:51:12 +03:00
Koitharu
464f24e9f0 Fix unwanted touch events when chapters sheet is collapsed 2025-10-21 15:46:34 +03:00
Koitharu
c8a8203c39 Add authors to filter 2025-10-21 14:45:10 +03:00
Koitharu
b414758f32 Improve saved filters 2025-10-21 12:22:30 +03:00
Koitharu
1181860e41 Improve saved filters 2025-10-20 14:18:08 +03:00
Koitharu
e35521f16f Fix code formatting 2025-10-20 09:08:04 +03:00
MuhamadSyabitHidayattulloh
5fb8ff53f9 Feat: Add Saved Filters Feature 2025-10-20 09:07:56 +03:00
Vicente
a66283d035 Backup Restore reading stats 2025-10-20 08:45:40 +03:00
Vicente
a1ba0b8c21 Backup scrobblings 2025-10-20 08:45:40 +03:00
Koitharu
f3b42b9a42 Small improvement for chapter toast setting 2025-10-20 08:42:02 +03:00
google-labs-jules[bot]
aa2f2c17fc feat(reader): Add setting to toggle chapter toast 2025-10-20 08:37:18 +03:00
Koitharu
ebc17b645b Fix build 2025-10-19 16:08:37 +03:00
Koitharu
cc14e1abcf Fix crash when fast go to background 2025-10-19 15:26:25 +03:00
Koitharu
b1b474e2e7 Update readme 2025-10-19 15:08:41 +03:00
Koitharu
8ca3bece5d Fix crashes 2025-10-19 14:53:31 +03:00
Frosted
90bd9023d5 Translated using Weblate (Turkish)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-10-19 14:41:29 +03:00
Максим Горпиніч
986627f24d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-10-19 14:41:29 +03:00
kota fujimi
cf2b8e2481 Translated using Weblate (Japanese)
Currently translated at 76.4% (669 of 875 strings)

Co-authored-by: kota fujimi <urakids@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2025-10-19 14:41:29 +03:00
Koitharu
b9435de5cd Update parsers 2025-10-19 14:33:31 +03:00
Koitharu
861c21faea Merge pull request #1673 from EmilieHascoet/Feat/auto-scroll-speed 2025-10-16 11:03:46 +03:00
Emilie Hascoët
9b4d014b21 Change valueTo from 0.95 to 0.97 in XML layout 2025-10-16 09:32:02 +02:00
Koitharu
c6da7de699 Fix backup rules 2025-10-15 15:24:02 +03:00
Koitharu
ef3aa40acc Fix some warnings 2025-10-14 16:25:28 +03:00
Koitharu
07af3ea703 Backup-restore fixes 2025-10-14 14:38:54 +03:00
Koitharu
391c8ab649 Fix sync issues 2025-10-14 14:04:17 +03:00
Koitharu
6b1885c89d Revert "Fix screen rotation causing reader to jump back to initial page"
This reverts commit aeb3732d75.
2025-10-14 13:49:02 +03:00
Koitharu
8423b48fb9 Revert "Fix page position loss when switching reader modes"
This reverts commit 5f879f6c83.
2025-10-14 13:49:02 +03:00
Koitharu
803c825d91 Fix ProgressUpdateUseCase 2025-10-14 13:46:16 +03:00
Koitharu
6a9682a077 Refactor reader sensitivity settings #1576 2025-10-14 10:23:47 +03:00
Koitharu
9197b9cc3a Merge branch 'devel' of github.com:puargs/Kotatsu into devel 2025-10-14 09:47:17 +03:00
google-labs-jules[bot]
02ea804874 Fix(reader): Fix incorrect scaling for short images in webtoon reader
When an image in the webtoon reader is shorter than the screen height, it was being incorrectly scaled, causing it to appear zoomed in or cropped.

This was caused by the `scrollTo` function in `WebtoonImageView.kt` calling `resetScaleAndCenter()` for images with a scroll range of zero. This method, from the underlying SubsamplingScaleImageView library, resets the image to a default scale instead of using the custom logic required by the reader.

The fix replaces the call to `resetScaleAndCenter()` with `scrollToInternal(0)`. This ensures that the custom scaling logic, which fits the image to the screen width, is applied consistently to all images, regardless of their height.
2025-10-14 09:45:41 +03:00
Koitharu
c424466198 Debounce Discord RPC 2025-10-14 09:44:18 +03:00
Quentinho199
18b312dde6 "fix/UI bug about finding chapter" 2025-10-14 09:44:01 +03:00
Koitharu
f78262b1a0 Udpate dependencies 2025-10-13 19:21:18 +03:00
Koitharu
c557a51c4d Fix loading local manga in some corner cases 2025-10-13 19:21:18 +03:00
kota fujimi
8995762935 Translated using Weblate (Japanese)
Currently translated at 76.0% (665 of 875 strings)

Co-authored-by: kota fujimi <urakids@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
Milo Ivir
ed2664db78 Translated using Weblate (Croatian)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-11 15:04:54 +03:00
Infy's Tagalog Translations
f5a5e53b5a Translated using Weblate (Filipino)
Currently translated at 99.0% (867 of 875 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
MuhamadSyabitHidayattulloh
9ef961590d Translated using Weblate (Indonesian)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-11 15:04:54 +03:00
Jinzhou Huang
9b569615ee Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 75.4% (660 of 875 strings)

Co-authored-by: Jinzhou Huang <2314662431@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
Antonio Sanchez Castellón
f48cf2efe4 Translated using Weblate (Spanish)
Currently translated at 98.5% (862 of 875 strings)

Co-authored-by: Antonio Sanchez Castellón <angelfx19@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
Ruthwik
18094a310c Translated using Weblate (Telugu)
Currently translated at 15.4% (135 of 875 strings)

Translated using Weblate (Telugu)

Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Co-authored-by: Ruthwik <rtwk03@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/te/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/te/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-11 15:04:54 +03:00
Anon
320c49a831 Translated using Weblate (Serbian)
Currently translated at 98.5% (862 of 875 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
return_null
2a971d5dae Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (875 of 875 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
Dragibus Noir
4467e79ae6 Translated using Weblate (French)
Currently translated at 100.0% (875 of 875 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-10-11 15:04:54 +03:00
Nataniel Dika Kurniawan
c68b180bf6 Translated using Weblate (Malay)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Malay)

Currently translated at 53.1% (465 of 875 strings)

Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-10-11 15:04:54 +03:00
copilot-swe-agent[bot]
5f879f6c83 Fix page position loss when switching reader modes
- Compare content.state with getCurrentState() to detect configuration changes vs intentional updates
- Use content.state when they match (mode switch case) to preserve explicit state updates
- Use getCurrentState() when they differ (rotation case) to restore saved position
- This ensures both screen rotation and mode switching work correctly

Co-authored-by: NathanBap <101987516+NathanBap@users.noreply.github.com>
2025-10-11 15:04:37 +03:00
copilot-swe-agent[bot]
aeb3732d75 Fix screen rotation causing reader to jump back to initial page
- Modified BaseReaderFragment to always use getCurrentState() when available
- getCurrentState() contains the most recent reading position saved in onPause/onDestroyView
- content.state may contain the initial state from when content was first loaded
- This ensures the current page position is preserved across configuration changes like screen rotation

Co-authored-by: NathanBap <101987516+NathanBap@users.noreply.github.com>
2025-10-11 15:04:37 +03:00
dragonx943
6292a0fd6b Fix margin 2025-10-11 14:46:34 +03:00
dragonx943
8985b4135d Make button in error dialog centered 2025-10-11 14:46:34 +03:00
Koitharu
f8a5397542 Update dependencies 2025-09-28 11:50:25 +03:00
Koitharu
5f51041220 Fix crash: add error handling for scrobbling info 2025-09-26 08:55:00 +03:00
Koitharu
5a14412b62 Fix appying webtoon pull gesture settings 2025-09-26 08:52:09 +03:00
Koitharu
be012f631a Update parsers 2025-09-23 18:50:44 +03:00
Koitharu
0165f43603 Fix crash 2025-09-23 18:43:20 +03:00
Koitharu
55801a1488 Update parsers 2025-09-21 13:28:38 +03:00
Koitharu
77103f016f Update parsers 2025-09-21 13:27:22 +03:00
DashZero
6b6719a259 Translated using Weblate (Thai)
Currently translated at 50.9% (446 of 875 strings)

Co-authored-by: DashZero <mee_original@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Joe
822642abb0 Translated using Weblate (Belarusian)
Currently translated at 99.6% (872 of 875 strings)

Co-authored-by: Joe <happenstance@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
தமிழ்நேரம்
260745fb95 Translated using Weblate (Tamil)
Currently translated at 100.0% (875 of 875 strings)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ta/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
MuhamadSyabitHidayattulloh
024ec0388f Translated using Weblate (Indonesian)
Currently translated at 100.0% (875 of 875 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Draken
5345998eec Translated using Weblate (Vietnamese)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (869 of 869 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Nicola Bortoletto
3d56190e71 Translated using Weblate (Italian)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (869 of 869 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Shams deen
954431d0a5 Added translation using Weblate (Arabic (Egyptian))
Added translation using Weblate (Arabic (Egyptian))

Co-authored-by: Shams deen <shamsdeen84@gmail.com>
2025-09-21 13:26:53 +03:00
return_null
afec63b443 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (869 of 869 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.5% (865 of 869 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Nataniel Dika Kurniawan
ac5b29c35a Translated using Weblate (Malay)
Currently translated at 51.7% (453 of 875 strings)

Translated using Weblate (Malay)

Currently translated at 51.6% (452 of 875 strings)

Translated using Weblate (Malay)

Currently translated at 49.5% (431 of 869 strings)

Translated using Weblate (Javanese)

Currently translated at 9.4% (82 of 869 strings)

Translated using Weblate (Javanese)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Malay)

Currently translated at 40.5% (352 of 869 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (869 of 869 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Javanese)

Added translation using Weblate (Javanese)

Translated using Weblate (Malay)

Currently translated at 38.4% (334 of 869 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (869 of 869 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/jv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/jv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-09-21 13:26:53 +03:00
Frosted
59f5578b66 Translated using Weblate (Turkish)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (869 of 869 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Roel v
391dbb4237 Translated using Weblate (Gothic)
Currently translated at 2.8% (25 of 869 strings)

Translated using Weblate (Gothic)

Currently translated at 44.4% (4 of 9 strings)

Added translation using Weblate (Gothic)

Added translation using Weblate (Gothic)

Co-authored-by: Roel v <roel11112@live.nl>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/got/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/got/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-09-21 13:26:53 +03:00
gekka
7d4505eb78 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.5% (865 of 869 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Максим Горпиніч
e6ceb20cf7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (869 of 869 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
lenn
8004f8c093 Translated using Weblate (Polish)
Currently translated at 97.8% (849 of 868 strings)

Co-authored-by: lenn <l3ennec@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-09-21 13:26:53 +03:00
Koitharu
61bf2abb6c Change inset handling in reader 2025-09-21 12:28:32 +03:00
Koitharu
d9612f3427 Webtoon pull gesture refactoring 2025-09-21 11:57:27 +03:00
Koitharu
435c3824f7 Remove A5 compatibility code 2025-09-20 09:56:03 +03:00
Koitharu
c846693570 Cleanup R8 rules 2025-09-20 09:37:23 +03:00
Koitharu
123937cd01 Fix SearchView closing on back pressed (close #1532, close #1487) 2025-09-20 09:31:10 +03:00
Koitharu
9f56554313 Reduce apk size 2025-09-20 07:55:27 +03:00
Koitharu
f8687bb697 Improve background WebView usage 2025-09-17 09:27:10 +03:00
Koitharu
43d3a2cc6a Fix crash on WebView.stopLoading() 2025-09-17 09:14:28 +03:00
MuhamadSyabitHidayattulloh
a95db6ed21 chore: Show description in offline mode (#1597) 2025-09-15 11:02:52 +07:00
Koitharu
fd0bb57338 Background captcha resolving 2025-09-14 20:11:55 +03:00
Koitharu
6b94bc2632 Update dependencies 2025-09-14 20:11:55 +03:00
Koitharu
c8b91599c6 Fix OkHttp initialization 2025-09-14 20:11:55 +03:00
Draken
3a8b0f9e93 Merge pull request #1586 from MuhamadSyabitHidayattulloh/feat/pull-gesture-navigate-chapter
feat: Add Pull Gesture Navigate Chapter
2025-09-13 23:23:12 +07:00
MuhamadSyabitHidayattulloh
17a0725666 feat: Realtime Favorite and Storage Badges 2025-09-13 09:27:46 +03:00
Draken
3be7848ad9 Revert distributionSha256Sum 2025-09-13 09:25:58 +03:00
dragonx943
08202c11a3 build: migrate to gradle 9 2025-09-13 09:25:58 +03:00
MuhamadSyabitHidayattulloh
5ef907d046 fix: Ui not visible if Control Panel show 2025-09-11 09:36:24 +07:00
MuhamadSyabitHidayattulloh
c3776ea3c6 feat: Add Pull Gesture Navigate Chapter 2025-09-10 14:41:28 +07:00
Koitharu
a624bffea3 Upgrade minSdk to 23 2025-09-07 10:56:40 +03:00
Koitharu
8f38b4fe30 Replace DummyParser with TestMangaRepository 2025-09-07 10:31:13 +03:00
Koitharu
71a2de5358 Update dependencies 2025-09-06 10:41:53 +03:00
Koitharu
5478f8fb59 Fix crash in ListSelectionController 2025-09-06 10:41:49 +03:00
ViAnh
5155c9a33d Reduce gaps between webtoon pages 2025-09-06 10:12:37 +03:00
puargs
1d1e49123a feat(reader): Add sensitivity setting for double-page mode
This commit introduces a new setting to control the scroll sensitivity of the double-page reader mode.

- A SeekBar has been added to the reader configuration sheet to adjust the sensitivity.
- The DoublePageSnapHelper now uses this setting to calculate the scroll distance.
- The setting is stored in AppSettings.
2025-09-04 22:40:08 -06:00
Koitharu
f7a461a9d8 Fix sync auth buttons (close #1556) 2025-08-30 09:25:55 +03:00
Aray LXa
3a02d22e02 Translated using Weblate (Persian)
Currently translated at 70.2% (610 of 868 strings)

Translated using Weblate (Persian)

Currently translated at 66.9% (581 of 868 strings)

Co-authored-by: Aray LXa <araylxa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2025-08-30 09:24:58 +03:00
Koitharu
2b8a29e2a6 Update parsers 2025-08-30 09:24:05 +03:00
Koitharu
bc68441585 Update parsers 2025-08-24 11:06:16 +03:00
Koitharu
1cc51b6a88 Refactor usage WebView for parsers 2025-08-24 10:39:23 +03:00
Kusou
fd5aca7252 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Kusou <orion26br@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Abay Emes
e447245fac Translated using Weblate (Kazakh)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Kazakh)

Currently translated at 62.5% (543 of 868 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-08-24 10:14:59 +03:00
Draken
5af0ee1c69 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Shayan
c02d1641ab Translated using Weblate (Persian)
Currently translated at 59.2% (514 of 868 strings)

Co-authored-by: Shayan <shayans31516@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Aray LXa
f55c525c8a Translated using Weblate (Persian)
Currently translated at 59.2% (514 of 868 strings)

Co-authored-by: Aray LXa <araylxa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Максим Горпиніч
a42fc87a9a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Juan Rubin
6b6905fd71 Translated using Weblate (Portuguese)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-08-24 10:14:59 +03:00
Koitharu
b7f57856db Author search support for external manga sources 2025-08-24 10:13:12 +03:00
Koitharu
1d6d626b62 Update parsers 2025-08-24 09:47:29 +03:00
Koitharu
d93ff92cc9 Update parsers and fix some deprecations 2025-08-16 08:11:44 +03:00
Koitharu
8eda113f3b Captcha group notification intent 2025-08-14 15:57:18 +03:00
Koitharu
3916c2619e Update parsers 2025-08-14 11:51:33 +03:00
Milo Ivir
1d3e8e55ca Translated using Weblate (Croatian)
Currently translated at 94.0% (816 of 868 strings)

Translated using Weblate (Croatian)

Currently translated at 92.7% (805 of 868 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Manuela Silva
2c3b4f29eb Translated using Weblate (Portuguese)
Currently translated at 97.8% (849 of 868 strings)

Co-authored-by: Manuela Silva <mmsrs@sky.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Marco Ramazzotti
ee530002b6 Translated using Weblate (Japanese)
Currently translated at 55.1% (479 of 868 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Marco Ramazzotti <cuordilava@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Lorenzo Stella
59d530e0dc Translated using Weblate (Italian)
Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Lorenzo Stella <lorenzo.stella.1408@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Nicola Bortoletto
52a132caed Translated using Weblate (Italian)
Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Макар Разин
379d2dd8d4 Translated using Weblate (Russian)
Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
R2E
f8cefa3e8d Translated using Weblate (Arabic)
Currently translated at 92.9% (807 of 868 strings)

Co-authored-by: R2E <mokhalad875@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Champ0999
5e1eda850c Translated using Weblate (Italian)
Currently translated at 99.8% (867 of 868 strings)

Co-authored-by: Champ0999 <champ0999@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
gekka
18cc0ad0fb Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.5% (864 of 868 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Kuraki
11dd49c626 Translated using Weblate (Turkish)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Kuraki <qkuraki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Draken
2ad8ab0258 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Dragibus Noir
4f8c5325a4 Translated using Weblate (French)
Currently translated at 100.0% (868 of 868 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-08-14 11:24:16 +03:00
Koitharu
6e181a59a3 Update sync auth activity ui 2025-08-14 10:39:06 +03:00
Koitharu
7a7d20dbf4 AutoFixService fixes 2025-08-10 16:09:26 +03:00
Koitharu
83d5f8e378 UI fixes 2025-08-05 15:50:39 +03:00
Koitharu
5ac9bad728 Fix MultiMutex unlock when cancelled 2025-08-05 14:02:46 +03:00
Дмитро Крук
a090965a2d Translated using Weblate (Ukrainian)
Currently translated at 99.1% (860 of 867 strings)

Co-authored-by: Дмитро Крук <dimka89050@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
yunyi
1e376754bc Added translation using Weblate (Baoulé)
Co-authored-by: yunyi <1967158164@qq.com>
2025-08-04 16:12:21 +03:00
Hidayat
2cdbe52056 Translated using Weblate (Indonesian)
Currently translated at 98.7% (856 of 867 strings)

Co-authored-by: Hidayat <elbert.herry11@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
fadom06
1e09ac3ecb Translated using Weblate (German)
Currently translated at 74.9% (650 of 867 strings)

Translated using Weblate (German)

Currently translated at 74.9% (650 of 867 strings)

Co-authored-by: fadom06 <fadom06@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Draken
acc76c931a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (867 of 867 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (864 of 864 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Infy's Tagalog Translations
59c12d35c1 Translated using Weblate (Filipino)
Currently translated at 99.3% (861 of 867 strings)

Translated using Weblate (Filipino)

Currently translated at 99.1% (857 of 864 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
周笑然
0e3cad1af1 Added translation using Weblate (Cantonese (Traditional Han script))
Added translation using Weblate (Cantonese (Traditional Han script))

Co-authored-by: 周笑然 <3140609186@qq.com>
2025-08-04 16:12:21 +03:00
Reptalica
ba8766b32d Translated using Weblate (Vietnamese)
Currently translated at 100.0% (863 of 863 strings)

Co-authored-by: Reptalica <reptalica20@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Dragibus Noir
35421cb71e Translated using Weblate (French)
Currently translated at 100.0% (867 of 867 strings)

Translated using Weblate (French)

Currently translated at 100.0% (863 of 863 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
aicha roun souleiman
8cecd9a0e2 Translated using Weblate (French)
Currently translated at 100.0% (863 of 863 strings)

Co-authored-by: aicha roun souleiman <louqman078@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Nicola Bortoletto
523057f3e1 Translated using Weblate (Italian)
Currently translated at 99.8% (866 of 867 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (861 of 863 strings)

Translated using Weblate (Italian)

Currently translated at 98.3% (849 of 863 strings)

Translated using Weblate (Italian)

Currently translated at 97.9% (844 of 862 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Frosted
337d196bc3 Translated using Weblate (Turkish)
Currently translated at 100.0% (867 of 867 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (867 of 867 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (863 of 863 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (862 of 862 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Hosted Weblate
c3b4c032bb Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
zmni
4590c753ed Translated using Weblate (Indonesian)
Currently translated at 99.8% (848 of 849 strings)

Co-authored-by: zmni <zmni@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Dragibus Noir
9733101f0c Translated using Weblate (French)
Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (French)

Currently translated at 100.0% (849 of 849 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Robert Broketa
8cd71cc98d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (849 of 849 strings)

Co-authored-by: Robert Broketa <robert@broketa.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Kanta Sekiguchi
42748d9c98 Translated using Weblate (Japanese)
Currently translated at 54.6% (464 of 849 strings)

Co-authored-by: Kanta Sekiguchi <kanta.sekiguchi360@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Bai
8043574314 Translated using Weblate (Turkish)
Currently translated at 100.0% (849 of 849 strings)

Co-authored-by: Bai <bai@baturax.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Shayan
44d1fdb9d3 Translated using Weblate (Persian)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Persian)

Currently translated at 32.9% (280 of 849 strings)

Co-authored-by: Shayan <shayans31516@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-08-04 16:12:21 +03:00
gekka
bc7054de4a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.5% (863 of 867 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.5% (860 of 864 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.5% (859 of 863 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.5% (858 of 862 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (843 of 848 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.4% (843 of 848 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Halbast Abdullah
4971e8ab0f Translated using Weblate (Kurdish (Central))
Currently translated at 2.5% (22 of 848 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 66.6% (6 of 9 strings)

Added translation using Weblate (Kurdish (Central))

Added translation using Weblate (Kurdish (Central))

Co-authored-by: Halbast Abdullah <halbastabdullah7@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ckb/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ckb/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-08-04 16:12:21 +03:00
Anon
df038b1edb Translated using Weblate (Serbian)
Currently translated at 99.5% (841 of 845 strings)

Translated using Weblate (Serbian)

Currently translated at 99.5% (841 of 845 strings)

Translated using Weblate (Serbian)

Currently translated at 99.5% (841 of 845 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Juan Rubin
7e7aabc1d1 Translated using Weblate (Portuguese)
Currently translated at 100.0% (845 of 845 strings)

Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Akhil Raj
9605ff89fb Translated using Weblate (Malayalam)
Currently translated at 4.7% (40 of 845 strings)

Translated using Weblate (Malayalam)

Currently translated at 4.7% (40 of 845 strings)

Translated using Weblate (Malayalam)

Currently translated at 3.5% (30 of 845 strings)

Translated using Weblate (Malayalam)

Currently translated at 3.5% (30 of 845 strings)

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ml/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Draken
4ed177d29f Translated using Weblate (Vietnamese)
Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (849 of 849 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (848 of 848 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (848 of 848 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (848 of 848 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (845 of 845 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Frosted
61cefefd10 Translated using Weblate (Turkish)
Currently translated at 100.0% (845 of 845 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-08-04 16:12:21 +03:00
Koitharu
9f965c5269 Remove Telegram bot token from public source 2025-08-04 16:11:28 +03:00
Koitharu
0c713cb799 Fix inifite captcha notifications 2025-08-04 12:50:09 +03:00
Koitharu
6d3f8cbb3b Filter for Local storage tab on main screen 2025-08-04 09:57:07 +03:00
Koitharu
05739bb5b3 Proper handling network unavailable error for images 2025-07-30 16:22:21 +03:00
Koitharu
47f0bbee17 Disk cache for favicons 2025-07-30 15:52:09 +03:00
Koitharu
dd77926dcb Improve sources settings 2025-07-30 14:50:45 +03:00
Koitharu
1b76f21507 Show reason why manga has no chapters 2025-07-30 14:06:45 +03:00
Koitharu
fe21af5443 Update parsers 2025-07-29 16:22:04 +03:00
Koitharu
0b0373021e Fix loading local manga 2025-07-26 19:28:32 +03:00
Koitharu
d641e7933d Option to show only downloaded chapters 2025-07-25 13:43:01 +03:00
Koitharu
d8efe374a8 Experimental: improve manga loading in reader 2025-07-24 15:20:42 +03:00
Koitharu
506a8b6e90 UI fixes 2025-07-23 12:08:27 +03:00
Koitharu
d81173bf76 Merge branch 'feature/discord_rpc' into devel 2025-07-22 16:42:35 +03:00
Koitharu
896452a096 Discord RPC improvements 2025-07-22 13:05:27 +03:00
Daniil Zhuravlev
35aa4d5e8f ci: add a site update trigger when the application is released 2025-07-21 08:59:34 +03:00
Koitharu
4d4c9c7a48 Discord RPC 2025-07-20 15:18:11 +03:00
Koitharu
b667e32598 Update parsers 2025-07-20 08:13:28 +03:00
Koitharu
c987fc234b Add LeakCanary to nighly builds 2025-07-17 20:42:12 +03:00
Koitharu
8142a6811b Add option to hide fab (close #1466) 2025-07-16 20:05:00 +03:00
Koitharu
3e36e1e11c Debug menu for debug builds 2025-07-16 20:05:00 +03:00
Koitharu
30aaca6341 Merge pull request #1497 from dragonx943/patch-1 2025-07-13 18:48:46 +03:00
Draken
43b34a7bca Fix gradle checksum 2025-07-13 21:04:31 +07:00
Koitharu
b23008d0ae Update Miku theme #1490 2025-07-13 11:23:15 +03:00
Koitharu
5a368b27bb Fix override applying 2025-07-13 11:07:00 +03:00
Koitharu
fe3f95d160 Cache custom covers (close #1492) 2025-07-13 10:04:18 +03:00
Koitharu
de1a297338 Fix downloading edited manga (close #1493) 2025-07-13 09:41:04 +03:00
Koitharu
d6350afe3a Upgrade target sdk 2025-07-13 09:35:24 +03:00
Koitharu
ec048c70f1 Update parsers 2025-07-10 21:39:46 +03:00
Dragibus Noir
282c1b51f7 Translated using Weblate (French)
Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Infy's Tagalog Translations
d6b6ce1bcd Translated using Weblate (Filipino)
Currently translated at 99.4% (838 of 843 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Giovanni S.C
f48444dcf6 Translated using Weblate (Spanish)
Currently translated at 92.6% (781 of 843 strings)

Translated using Weblate (Spanish)

Currently translated at 92.6% (781 of 843 strings)

Co-authored-by: Giovanni S.C <giovanniandre2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Krays The Poet
15ba766643 Translated using Weblate (German)
Currently translated at 76.5% (645 of 843 strings)

Translated using Weblate (German)

Currently translated at 76.5% (645 of 843 strings)

Co-authored-by: Krays The Poet <kraysthepoet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Draken
a0dbbcb350 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (845 of 845 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (843 of 843 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Frosted
f72bba9557 Translated using Weblate (Turkish)
Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Макар Разин
207791aa3e Translated using Weblate (Ukrainian)
Currently translated at 99.6% (840 of 843 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.6% (840 of 843 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (843 of 843 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Koitharu
6319997716 Periodical backup improvements 2025-07-10 09:54:45 +03:00
Koitharu
b70c1da54b Implement missing getForegroundInfo for LocalStorageCleanupWorker 2025-07-10 08:57:11 +03:00
Koitharu
621cb19c5b Fix saving override for non-library manga 2025-07-10 08:48:10 +03:00
Koitharu
b528b7b3c1 Fix passing headers to favicon requests 2025-07-09 22:07:37 +03:00
Koitharu
9a1bb6f6fc Fix long tap on old Android versions (close #1478) 2025-07-09 21:57:37 +03:00
Koitharu
37f9c4b9f6 Fix window insets and search closing 2025-07-09 21:42:28 +03:00
Koitharu
d0084e50e7 Fix loading local manga (closes #1481, #1474, #1479, #1484, #1439) 2025-07-09 21:21:02 +03:00
Koitharu
088576cc9d Ignore network error for background progress update 2025-07-07 13:46:37 +03:00
Koitharu
f0ba42b518 Fix captcha notification dismissing 2025-07-06 12:09:30 +03:00
Koitharu
33366e63db Translated using Weblate (Russian)
Currently translated at 99.7% (841 of 843 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Fênixsmasters
0f39b313c0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (842 of 842 strings)

Co-authored-by: Fênixsmasters <amadeudavi373@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Muktii
abcbb940d3 Translated using Weblate (Indonesian)
Currently translated at 99.2% (836 of 842 strings)

Co-authored-by: Muktii <mhdmuktikece@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
mak7im01
f4fec709fc Translated using Weblate (Russian)
Currently translated at 99.1% (835 of 842 strings)

Co-authored-by: mak7im01 <mak7im02@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Nagito Luck
a8c6f6a1ce Translated using Weblate (Spanish)
Currently translated at 92.0% (775 of 842 strings)

Co-authored-by: Nagito Luck <dealbamax@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Giovanni S.C
3a6794a50e Translated using Weblate (Spanish)
Currently translated at 92.0% (775 of 842 strings)

Co-authored-by: Giovanni S.C <giovanniandre2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Justine Kyle Cobar
aea3328f2d Translated using Weblate (Filipino)
Currently translated at 99.0% (834 of 842 strings)

Co-authored-by: Justine Kyle Cobar <cobarjustinekyle583@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Nagito Luck
c225e90626 Translated using Weblate (Spanish)
Currently translated at 90.7% (764 of 842 strings)

Co-authored-by: Nagito Luck <dealbamax@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Giovanni S.C
de8500d705 Translated using Weblate (Spanish)
Currently translated at 90.7% (764 of 842 strings)

Co-authored-by: Giovanni S.C <giovanniandre2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Draken
2d41cf14e2 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (842 of 842 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (840 of 840 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Frosted
1777528fcb Translated using Weblate (Turkish)
Currently translated at 100.0% (842 of 842 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (840 of 840 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Geovani Amaral
43618ed224 Translated using Weblate (Portuguese)
Currently translated at 100.0% (840 of 840 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.5% (836 of 840 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.4% (835 of 840 strings)

Co-authored-by: Geovani Amaral <geovani.af4@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-07-05 14:59:45 +03:00
Koitharu
2229439547 Hide chapter-pages tabs when action mode started (closes #1454) 2025-07-05 10:24:34 +03:00
Koitharu
98abffa67b Fix login autofill (closes #1469) 2025-07-05 10:24:34 +03:00
Koitharu
aafefffc27 Yellow color filter in reader 2025-07-05 10:24:34 +03:00
Koitharu
add5c7dc17 Update parsers 2025-07-05 10:24:33 +03:00
Koitharu
40584cb6f5 Fix back press exit confirmation 2025-07-05 10:24:33 +03:00
Koitharu
e549d141a4 Merge pull request #1472 from dragonx943/patch-1 2025-07-04 09:21:19 +03:00
Draken
4199f54241 Update tags_warnlist 2025-07-04 12:07:33 +07:00
Stanislav Khromov
e511c3cc97 Update strings.xml 2025-06-30 08:49:12 +03:00
Stanislav Khromov
33dbca1bc9 Remove navigation inversion for tap actions
Tap actions for page and chapter navigation no longer respect the 'Invert navigation controls' setting. The setting summary was updated to clarify it only affects volume button and hardware key navigation.
2025-06-30 08:49:12 +03:00
Stanislav Khromov
40778a88dd Add option to invert reader navigation controls
Introduces a new setting allowing users to invert navigation controls for page and chapter switching in the reader. Updates preferences, strings, and control logic to support swapping the direction of volume button and tap navigation.
2025-06-30 08:49:12 +03:00
Koitharu
3e8e423962 Merge remote-tracking branch 'weblate/devel' into devel 2025-06-29 16:59:22 +03:00
Koitharu
881473f495 Update parsers 2025-06-29 16:39:45 +03:00
M
dc8ecf2b12 Translated using Weblate (Literary Chinese)
Currently translated at 5.0% (42 of 838 strings)

Translated using Weblate (Literary Chinese)

Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: M <recall-ditto-giddy@duck.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/lzh/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lzh/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-06-29 15:11:24 +02:00
6XChen
19d7a98968 Translated using Weblate (Indonesian)
Currently translated at 99.7% (836 of 838 strings)

Co-authored-by: 6XChen <blackdarksoulweapon@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-06-29 15:11:23 +02:00
Sapate Vaibhav
b2538065a9 Translated using Weblate (Hindi)
Currently translated at 79.3% (665 of 838 strings)

Co-authored-by: Sapate Vaibhav <sapatevaibhav@duck.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2025-06-29 15:11:22 +02:00
maryush
ebf37e3d08 Translated using Weblate (Polish)
Currently translated at 100.0% (840 of 840 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (838 of 838 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-06-29 15:11:21 +02:00
Alvoracz
8c37135a40 Translated using Weblate (Czech)
Currently translated at 98.0% (820 of 836 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-06-29 15:11:20 +02:00
finnchiki
ca716f9a34 Translated using Weblate (Indonesian)
Currently translated at 98.2% (821 of 836 strings)

Co-authored-by: finnchiki <azrellgans@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-06-29 15:11:19 +02:00
TheOneWhoCares
112ac70648 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (836 of 836 strings)

Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-06-29 15:11:18 +02:00
Dragibus Noir
694e4d4baf Translated using Weblate (French)
Currently translated at 100.0% (840 of 840 strings)

Translated using Weblate (French)

Currently translated at 100.0% (836 of 836 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-06-29 15:11:17 +02:00
Rafa Herzog
e74afa06a1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Rafa Herzog <49111482+necronyxon@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-06-29 15:11:16 +02:00
Infy's Tagalog Translations
4494cc1888 Translated using Weblate (Filipino)
Currently translated at 99.5% (832 of 836 strings)

Translated using Weblate (Filipino)

Currently translated at 99.5% (830 of 834 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-06-29 15:11:15 +02:00
Draken
b13d9078d4 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-06-29 15:11:14 +02:00
gekka
5999301de8 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (837 of 840 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (836 of 838 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (834 of 836 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (832 of 834 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (832 of 834 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-06-29 15:11:13 +02:00
Frosted
601b4016e6 Translated using Weblate (Turkish)
Currently translated at 100.0% (838 of 838 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-06-29 15:11:12 +02:00
Nicola Bortoletto
4d13b8f7b0 Translated using Weblate (Italian)
Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-06-29 15:11:11 +02:00
Макар Разин
6b95ec829e Translated using Weblate (Russian)
Currently translated at 100.0% (834 of 834 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-06-29 15:11:10 +02:00
M
1c658fa3c3 Translated using Weblate (Literary Chinese)
Currently translated at 5.0% (42 of 838 strings)

Translated using Weblate (Literary Chinese)

Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: M <recall-ditto-giddy@duck.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/lzh/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lzh/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-06-29 16:11:09 +03:00
6XChen
32a8a8fed2 Translated using Weblate (Indonesian)
Currently translated at 99.7% (836 of 838 strings)

Co-authored-by: 6XChen <blackdarksoulweapon@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Sapate Vaibhav
c63e0542a0 Translated using Weblate (Hindi)
Currently translated at 79.3% (665 of 838 strings)

Co-authored-by: Sapate Vaibhav <sapatevaibhav@duck.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
maryush
dea779fc4b Translated using Weblate (Polish)
Currently translated at 100.0% (840 of 840 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (838 of 838 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Alvoracz
339b8f7311 Translated using Weblate (Czech)
Currently translated at 98.0% (820 of 836 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
finnchiki
3c087dde11 Translated using Weblate (Indonesian)
Currently translated at 98.2% (821 of 836 strings)

Co-authored-by: finnchiki <azrellgans@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
TheOneWhoCares
dd1223229d Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (836 of 836 strings)

Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Dragibus Noir
0b393b0e81 Translated using Weblate (French)
Currently translated at 100.0% (836 of 836 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Rafa Herzog
b24cac9305 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Rafa Herzog <49111482+necronyxon@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Infy's Tagalog Translations
dc92723526 Translated using Weblate (Filipino)
Currently translated at 99.5% (832 of 836 strings)

Translated using Weblate (Filipino)

Currently translated at 99.5% (830 of 834 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Draken
203020e100 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
gekka
05eac1eabd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (837 of 840 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (836 of 838 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (834 of 836 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (832 of 834 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (832 of 834 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Frosted
c7b720ec91 Translated using Weblate (Turkish)
Currently translated at 100.0% (838 of 838 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Nicola Bortoletto
58026b6fc0 Translated using Weblate (Italian)
Currently translated at 100.0% (836 of 836 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Макар Разин
ba2f9dc16c Translated using Weblate (Russian)
Currently translated at 100.0% (834 of 834 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (834 of 834 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-06-29 16:11:09 +03:00
Stanislav Khromov
97afa29785 Improve edge detection performance (#1457)
* Optimize edge detection by decoding full image once

Refactors edge detection to decode the entire image at a downscaled resolution once, instead of decoding multiple regions. This improves performance by reducing repeated decoding operations and leverages direct pixel access for edge analysis. Adds a scale factor calculation to balance accuracy and speed for large images.

* Update EdgeDetector.kt

* Update EdgeDetector.kt

* Update EdgeDetector.kt
2025-06-29 16:10:48 +03:00
Koitharu
97f2ff3bbd Adjust AlertDialog style 2025-06-29 16:08:55 +03:00
Koitharu
14f185393b New mirror switching approach 2025-06-28 09:12:49 +03:00
Koitharu
fc7f5f2cf9 Fix captcha notification dismissing 2025-06-28 07:02:51 +03:00
Koitharu
4088f50120 Remove unrelevant strictmode detections 2025-06-28 06:55:58 +03:00
Koitharu
afa18e086c Update tg bot token 2025-06-28 06:53:34 +03:00
Koitharu
a8cfb3521c Fix window size handling in reader 2025-06-28 06:52:41 +03:00
Koitharu
7047ee6155 Fix crashes 2025-06-27 19:25:55 +03:00
Koitharu
957b12f338 Improve error reporting from notifications 2025-06-27 18:39:10 +03:00
Koitharu
679b1fd2f2 Fix crashes 2025-06-26 18:56:20 +03:00
Koitharu
367a917c56 Update color schemes 2025-06-26 15:42:32 +03:00
Koitharu
ed7fdb32a1 Handle more cover loading errors 2025-06-24 18:45:27 +03:00
Koitharu
e8f0aa8388 Fix biometric authentication launch 2025-06-24 18:24:40 +03:00
Koitharu
7404612a84 Fix crash in downloads 2025-06-24 18:12:58 +03:00
Koitharu
512069ca3e Fix local manga list order by date 2025-06-23 21:15:24 +03:00
Koitharu
b53b8eefa3 AppBar behavior fixes 2025-06-23 20:59:14 +03:00
Koitharu
f7509b09c1 Fix amoled theme 2025-06-23 20:30:32 +03:00
Koitharu
fbff0ab027 Fix Arabic flag mapping (closes #1440) 2025-06-23 20:09:15 +03:00
Koitharu
17656233ef Fix bookmarks images loading 2025-06-22 20:32:37 +03:00
Koitharu
18466a2c1a Show error codes on covers 2025-06-22 19:54:39 +03:00
Koitharu
2797ea6a99 Fix backup backward compatibility 2025-06-22 18:47:20 +03:00
Koitharu
b4a298ea55 Fix empty shortcut title crash 2025-06-22 18:22:08 +03:00
Koitharu
f811eeebc9 Scroll timer improvements 2025-06-22 15:59:57 +03:00
Koitharu
aeee782512 Retry thumbnails loading after network state changed 2025-06-22 13:28:44 +03:00
Koitharu
fe59a13218 Update filter header UI 2025-06-22 12:39:25 +03:00
Koitharu
c2688517ba Improve assistant integration 2025-06-22 11:44:22 +03:00
Koitharu
95019f9eb6 Option to open reader in a separated task 2025-06-22 11:30:28 +03:00
Koitharu
f43769bde7 Fix double reader mode switch 2025-06-22 10:59:42 +03:00
Koitharu
c576b62d51 Scrobbling improvements and fixes (closes #1448) 2025-06-22 10:25:48 +03:00
Koitharu
722c4466bf Update parsers 2025-06-21 19:55:34 +03:00
Koitharu
61b863ae96 Fix DateTimeAgo in future 2025-06-21 18:15:40 +03:00
Koitharu
55ea0d7b2b Fix InflateException crash on A15 (maybe) 2025-06-21 18:06:40 +03:00
Koitharu
04f56c6d84 Update dependencies 2025-06-21 17:54:23 +03:00
Koitharu
e7aae4e72a Remove grid size slider step 2025-06-21 17:41:48 +03:00
Koitharu
3547e7afb8 Fix search suggestions on landscape 2025-06-21 16:26:28 +03:00
Koitharu
a07e5ab278 Fix filter summary 2025-06-21 16:26:04 +03:00
Koitharu
1ddc32cbd4 Fix crash in BackupRepository 2025-06-21 16:11:13 +03:00
Koitharu
80a30d059f Fix json configuration for backups 2025-06-20 09:22:15 +03:00
Koitharu
437e6809bf Backup restorng fixes 2025-06-14 15:04:10 +03:00
Koitharu
b9d4c070eb Use streams for backups 2025-06-14 12:07:52 +03:00
Koitharu
4ef6908e82 Use kotlin serialization for sync 2025-06-09 12:43:33 +03:00
Koitharu
b854ca8807 Update dependencies 2025-06-07 11:46:37 +03:00
Koitharu
db89bdfdff Respect network data saver #1390 2025-06-07 10:14:51 +03:00
Koitharu
dc1df527b2 Set read button min width 2025-06-07 09:11:50 +03:00
Koitharu
584e93fbbf Option to edit manga in details screen 2025-06-07 08:45:22 +03:00
Koitharu
c2d4258afc Improve external plugins support 2025-06-07 08:31:47 +03:00
Koitharu
60dca5f8c3 Page image picker; ability to use manga page as custom cover 2025-06-05 21:22:07 +03:00
Koitharu
5d1afab071 Fix workers scheduling 2025-06-05 09:31:29 +03:00
Koitharu
851e417370 Revert "Remove redundant workers constraints to fix crash"
This reverts commit 71a82ae187.
2025-06-05 09:28:37 +03:00
Koitharu
71a82ae187 Remove redundant workers constraints to fix crash 2025-06-03 19:13:11 +03:00
Koitharu
0e54e4778e Merge pull request #1430 from dragonx943/patch-1 2025-06-03 11:21:39 +03:00
Draken
053ce880e4 Fix build 2025-06-03 15:14:37 +07:00
VardanRattan
67b1e4e862 Fix ForegroundServiceStartNotAllowedException on Android 12+
- Add setExpedited() to WorkManager requests missing it
- Fixes crash when starting foreground services from background
- Updated DownloadFactory, LocalStorageCleanupWorker, SuggestionsWorker, and TrackWorker schedulers
- Ensures compliance with Android 12+ foreground service restrictions
2025-05-31 17:41:46 +03:00
Koitharu
e04a877310 Fix favorite categories observing 2025-05-31 15:57:49 +03:00
Koitharu
48a605eeb0 Update dependencies 2025-05-31 08:45:57 +03:00
Koitharu
aed08f18bb Merge branch 'master' into devel 2025-05-31 08:42:06 +03:00
dragonx943
0c626cd2a3 Remove theme_name_dynamic_v2 string 2025-05-31 08:25:43 +03:00
Draken
82e6aa335b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (833 of 833 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-05-31 08:25:22 +03:00
Koitharu
f79575e8d5 Option to not collapse descriptions 2025-05-31 08:24:02 +03:00
Koitharu
ac0dc0a94a Added data to details intent 2025-05-31 07:51:12 +03:00
Koitharu
7de4ac2b89 Fix tablet navigation insets 2025-05-29 20:41:31 +03:00
Koitharu
e01ddc0db7 Fix Read split button #1417 2025-05-28 18:50:58 +03:00
Koitharu
5745eca683 Merge branch 'devel' of https://hosted.weblate.org/git/kotatsu/strings into devel 2025-05-28 18:38:53 +03:00
Dragibus Noir
8a8ee46234 Translated using Weblate (French)
Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Rodrigo Cunha
26489627f2 Translated using Weblate (Portuguese)
Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Rodrigo Cunha <rodrigocunha1110@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Naga
341ced2d83 Translated using Weblate (French)
Currently translated at 99.6% (826 of 829 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
maryush
c5dd0eb375 Translated using Weblate (Polish)
Currently translated at 100.0% (826 of 826 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
TheOneWhoCares
33045ae36f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (825 of 825 strings)

Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Макар Разин
442ebe5919 Translated using Weblate (Russian)
Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.8% (824 of 825 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Belarusian)

Currently translated at 97.0% (800 of 824 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Yauhen
363bcbad18 Translated using Weblate (Belarusian)
Currently translated at 89.8% (740 of 824 strings)

Co-authored-by: Yauhen <bugomol@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
sithuhein
5721cf71d3 Translated using Weblate (Burmese)
Currently translated at 5.8% (48 of 824 strings)

Added translation using Weblate (Burmese)

Translated using Weblate (Burmese)

Currently translated at 77.7% (7 of 9 strings)

Co-authored-by: sithuhein <sithuh3in2007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/my/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/my/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-05-27 17:25:02 +03:00
Roger VC
ed8cc8d01f Translated using Weblate (Catalan)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Roger VC <rogervilarasau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ca/
Translation: Kotatsu/plurals
2025-05-27 17:25:02 +03:00
Evgeniy Khramov
6c38f59e0f Translated using Weblate (Russian)
Currently translated at 99.6% (817 of 820 strings)

Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Infy's Tagalog Translations
4d07311afc Translated using Weblate (Filipino)
Currently translated at 99.7% (825 of 827 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (819 of 820 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Lumiini
8ec81bb33f Translated using Weblate (Finnish)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Finnish)

Currently translated at 30.8% (253 of 820 strings)

Co-authored-by: Lumiini <bennokaynak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-05-27 17:25:02 +03:00
Draken
eb322d0dcd Translated using Weblate (Vietnamese)
Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (820 of 820 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Ruslik31
42a929d3f1 Translated using Weblate (Russian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Ruslik31 <dolgovruslana0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Vermeil
978167ad3f Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Vermeil <krisgamer6677@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Zan 1456
9767e1a87d Translated using Weblate (Hungarian)
Currently translated at 77.5% (632 of 815 strings)

Co-authored-by: Zan 1456 <mestermc594@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Brayan González
4b822d6684 Translated using Weblate (Spanish)
Currently translated at 93.6% (763 of 815 strings)

Co-authored-by: Brayan González <bg2896054brayan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Baldomero, Kier Justine D
b59e41ef62 Translated using Weblate (Filipino)
Currently translated at 99.8% (814 of 815 strings)

Co-authored-by: Baldomero, Kier Justine D <yeartwothousandfive@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Anon
89ddfd3037 Translated using Weblate (Serbian)
Currently translated at 99.5% (822 of 826 strings)

Translated using Weblate (Serbian)

Currently translated at 98.4% (802 of 815 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
gekka
d97a2bba52 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (824 of 827 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (824 of 825 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (820 of 821 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (819 of 820 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (814 of 815 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (812 of 815 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (812 of 815 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Infy's Tagalog Translations
e72a8b2b8e Translated using Weblate (Filipino)
Currently translated at 99.7% (813 of 815 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Boqirz
c3c1d94f92 Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Boqirz <alveromodar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Frosted
1f8c5a894a Translated using Weblate (Turkish)
Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (827 of 827 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (826 of 826 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Nicola Bortoletto
9dd05fcc70 Translated using Weblate (Italian)
Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Italian)

Currently translated at 99.8% (828 of 829 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (827 of 827 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (826 of 826 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-05-27 17:25:02 +03:00
Dragibus Noir
e6487fb199 Translated using Weblate (French)
Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Rodrigo Cunha
9d9f611091 Translated using Weblate (Portuguese)
Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Rodrigo Cunha <rodrigocunha1110@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Naga
a7c21515cd Translated using Weblate (French)
Currently translated at 99.6% (826 of 829 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
maryush
01f1a37bc1 Translated using Weblate (Polish)
Currently translated at 100.0% (826 of 826 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
TheOneWhoCares
5a24f43db3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (825 of 825 strings)

Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Макар Разин
61b2e96bf1 Translated using Weblate (Russian)
Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.8% (824 of 825 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Belarusian)

Currently translated at 97.0% (800 of 824 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Yauhen
affbd0cdb6 Translated using Weblate (Belarusian)
Currently translated at 89.8% (740 of 824 strings)

Co-authored-by: Yauhen <bugomol@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
sithuhein
a2c40a302b Translated using Weblate (Burmese)
Currently translated at 5.8% (48 of 824 strings)

Added translation using Weblate (Burmese)

Translated using Weblate (Burmese)

Currently translated at 77.7% (7 of 9 strings)

Co-authored-by: sithuhein <sithuh3in2007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/my/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/my/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-05-27 16:24:54 +02:00
Roger VC
6ac6353e6a Translated using Weblate (Catalan)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Roger VC <rogervilarasau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ca/
Translation: Kotatsu/plurals
2025-05-27 16:24:54 +02:00
Evgeniy Khramov
203dc1801a Translated using Weblate (Russian)
Currently translated at 99.6% (817 of 820 strings)

Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Infy's Tagalog Translations
dc4fbf61a9 Translated using Weblate (Filipino)
Currently translated at 99.6% (830 of 833 strings)

Translated using Weblate (Filipino)

Currently translated at 99.7% (825 of 827 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (819 of 820 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Lumiini
1fac005db7 Translated using Weblate (Finnish)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Finnish)

Currently translated at 30.8% (253 of 820 strings)

Co-authored-by: Lumiini <bennokaynak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-05-27 16:24:54 +02:00
Draken
e9ee658385 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (831 of 831 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (820 of 820 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Ruslik31
0785ba70ce Translated using Weblate (Russian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Ruslik31 <dolgovruslana0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Vermeil
c4c6867fef Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Vermeil <krisgamer6677@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Zan 1456
447a44208f Translated using Weblate (Hungarian)
Currently translated at 77.5% (632 of 815 strings)

Co-authored-by: Zan 1456 <mestermc594@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Brayan González
eb9bd2ad5f Translated using Weblate (Spanish)
Currently translated at 93.6% (763 of 815 strings)

Co-authored-by: Brayan González <bg2896054brayan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Baldomero, Kier Justine D
5030d2c4c0 Translated using Weblate (Filipino)
Currently translated at 99.8% (814 of 815 strings)

Co-authored-by: Baldomero, Kier Justine D <yeartwothousandfive@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Anon
6b9fc7dd50 Translated using Weblate (Serbian)
Currently translated at 99.5% (822 of 826 strings)

Translated using Weblate (Serbian)

Currently translated at 98.4% (802 of 815 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
gekka
c7ca0d9707 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (824 of 827 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (824 of 825 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (823 of 824 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (820 of 821 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (819 of 820 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (814 of 815 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (812 of 815 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (812 of 815 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Infy's Tagalog Translations
e9a38d0d03 Translated using Weblate (Filipino)
Currently translated at 99.7% (813 of 815 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Boqirz
bc6ce75268 Translated using Weblate (Indonesian)
Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Boqirz <alveromodar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Frosted
e545f19339 Translated using Weblate (Turkish)
Currently translated at 100.0% (833 of 833 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (827 of 827 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (826 of 826 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
Nicola Bortoletto
ac4682d62c Translated using Weblate (Italian)
Currently translated at 100.0% (833 of 833 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Italian)

Currently translated at 99.8% (828 of 829 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (827 of 827 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (826 of 826 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (821 of 821 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (820 of 820 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-05-27 16:24:54 +02:00
arasseo.
3afb446564 only update domain 2025-05-27 17:24:49 +03:00
Koitharu
0ed2232ac2 Update parsers 2025-05-27 17:03:14 +03:00
Koitharu
8d9129daaf Fix serivce starting crash on startup
(cherry picked from commit 157d5e6c05)
2025-05-27 16:57:01 +03:00
Koitharu
f799606688 Fix snackbar positioning
(cherry picked from commit b1497f2ace)
2025-05-27 16:56:24 +03:00
Koitharu
64adc4f58d Fix race condition while js evaluation
(cherry picked from commit 41d7fd1b86)
2025-05-27 16:56:18 +03:00
Koitharu
f6aad3355a Update parsers
(cherry picked from commit 19d0fe97a0)
2025-05-27 16:56:14 +03:00
Koitharu
0badf10a8b Fix WebView crash
(cherry picked from commit fa37c72923)
2025-05-27 16:54:56 +03:00
Koitharu
e5118f5266 Amoled background for search view 2025-05-27 16:47:10 +03:00
Koitharu
157d5e6c05 Fix serivce starting crash on startup 2025-05-26 18:25:17 +03:00
Koitharu
a02a8ff9db Fix WebView crash 2025-05-26 18:21:57 +03:00
Koitharu
b1497f2ace Fix snackbar positioning 2025-05-25 19:54:56 +03:00
Koitharu
099590c419 AdBlock for WebView 2025-05-25 19:30:55 +03:00
Koitharu
41d7fd1b86 Fix race condition while js evaluation 2025-05-25 10:05:10 +03:00
Koitharu
d3d7912bb8 Improve tablet navigation 2025-05-25 09:56:31 +03:00
Koitharu
12f1ffd019 Handle search action 2025-05-24 21:26:40 +03:00
Koitharu
19d0fe97a0 Update parsers 2025-05-24 21:13:33 +03:00
Koitharu
771954ffb8 Fix main activity navigation icon click 2025-05-24 10:29:34 +03:00
Koitharu
f4997f5a7f Improve captcha notifications 2025-05-24 10:18:18 +03:00
Koitharu
ff5a873d3b Fix main menu 2025-05-23 07:25:33 +03:00
Koitharu
1b5720f2a5 Migrate to mdc search view 2025-05-22 18:48:56 +03:00
Koitharu
a52730fff0 Improve mouse accessibility 2025-05-21 14:01:42 +03:00
Koitharu
2dfc9b75a2 Merge branch 'master' into devel 2025-05-19 19:55:39 +03:00
Koitharu
cc6f004e0e Fix themes 2025-05-19 19:49:34 +03:00
Koitharu
fa37c72923 Fix WebView crash 2025-05-19 19:11:46 +03:00
Koitharu
ab2235d0ca Update parsers 2025-05-18 14:40:45 +03:00
Koitharu
cbf707b403 Fix locales config
(cherry picked from commit 61c068d4ee)
2025-05-18 14:36:09 +03:00
Koitharu
8971c7a6a2 Fix color filte activity strings
(cherry picked from commit 8f8abcc3f6)
2025-05-18 14:35:19 +03:00
Koitharu
1576c9cdde Fix search menu item duplication
(cherry picked from commit a4b9acd622)
2025-05-18 14:34:44 +03:00
Koitharu
a0b8603510 Update dependencies 2025-05-18 14:32:01 +03:00
Koitharu
5b899b16d0 Update expressive theme 2025-05-15 19:45:51 +03:00
Koitharu
a4b9acd622 Fix search menu item duplication 2025-05-15 19:37:46 +03:00
Koitharu
c458f1eafb Show changelog in settings 2025-05-15 19:29:40 +03:00
Koitharu
8f8abcc3f6 Fix color filte activity strings 2025-05-15 17:45:21 +03:00
Koitharu
b4b9f90edc Improve mouse accessibility 2025-05-12 20:30:19 +03:00
Koitharu
7cc777f0a6 Show chapters count for branches 2025-05-12 19:34:29 +03:00
Koitharu
61c068d4ee Fix locales config 2025-05-12 18:53:36 +03:00
Koitharu
ff021b56f4 Styles fixes 2025-05-11 19:18:39 +03:00
Koitharu
94ef64c4b7 Improve mouse interaction 2025-05-11 18:46:05 +03:00
Koitharu
8ad28fd509 Fix keyboard navigation direction 2025-05-11 17:35:45 +03:00
Koitharu
7148ebcf34 Cleanup themes 2025-05-11 17:33:54 +03:00
Koitharu
1229e9626e Move bookmark button to bottom reader bar 2025-05-11 16:26:17 +03:00
Koitharu
4ec9a91644 Save bookmark images 2025-05-11 15:16:49 +03:00
Koitharu
1bbe1204e6 Update parsers 2025-05-11 14:25:22 +03:00
Koitharu
3aaddfd513 Theme fixes 2025-05-10 12:59:45 +03:00
Koitharu
f5514728fe Update themes inheritance structure 2025-05-08 21:56:23 +03:00
Koitharu
4fcb3a969b Fix per-app locale selection 2025-05-08 21:56:22 +03:00
Koitharu
23f3182769 Update parsers 2025-05-08 21:56:22 +03:00
Draken
b84e10e69f [Readme] Update informations 2025-05-08 19:32:20 +03:00
Draken
ce3a1969c8 [Readme] Update infomations 2025-05-08 19:32:20 +03:00
Draken
8282ca7d60 [Readme] Add certificate fingerprints 2025-05-08 19:32:20 +03:00
kadirkid
104d8da655 Switch per language support to manual
The current automatic support setup has a bug where the app language will change for users with Android 15 when there is a configuration change like rotating a screen. It seems that that using generateLocaleConfig on AGP 8.8+ triggers a bug in Android 15 (android:defaultLocale) which causes this issue
2025-05-08 19:14:54 +03:00
Koitharu
52c39ad40c Ask for one-time incognito for nsfw manga 2025-05-03 10:55:17 +03:00
Koitharu
842ecaaff6 Merge branch 'master' into devel 2025-05-03 09:26:21 +03:00
Koitharu
8d325aea0a Update details info card color 2025-05-03 09:16:23 +03:00
Koitharu
6cb090309a Merge pull request #1392 from dragonx943/patch-1
Update "dangerous" tags list
2025-05-03 08:30:55 +03:00
Draken
8d78b19128 Update tags_warnlist 2025-05-02 21:17:22 +07:00
Koitharu
5d890cb3d0 AVIF images downsampling 2025-05-02 14:38:12 +03:00
Koitharu
257f583f78 Fix image loading 2025-04-30 17:27:41 +03:00
Koitharu
d45bab3879 Fix reader activity ui 2025-04-30 16:14:32 +03:00
Koitharu
c871255eb7 Improve reader scroll timer 2025-04-28 19:08:11 +03:00
Koitharu
1a8045b89f Fix search suggestions 2025-04-27 15:28:48 +03:00
Koitharu
f91f55fa66 Fix warnings and code cleanup 2025-04-26 09:31:05 +03:00
Koitharu
10bd46f077 Refactor image loading 2025-04-25 19:50:48 +03:00
Koitharu
bd4fecc3b6 Option to override manga title and cover 2025-04-20 17:20:42 +03:00
Koitharu
d542fa6bb6 Update database structure and migrate from kapt to ksp 2025-04-15 20:07:26 +03:00
738 changed files with 23857 additions and 13221 deletions

View File

@@ -4,7 +4,7 @@ root = true
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4

View File

@@ -0,0 +1,16 @@
name: Trigger Site Update
on:
release:
types: [published]
jobs:
trigger-site:
runs-on: ubuntu-latest
steps:
- name: Send repository_dispatch to site-repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SITE_REPO_TOKEN }}
repository: KotatsuApp/website
event-type: app-release

3
.gitignore vendored
View File

@@ -6,6 +6,7 @@
/.idea/dictionaries
/.idea/modules.xml
/.idea/misc.xml
/.idea/markdown.xml
/.idea/discord.xml
/.idea/compiler.xml
/.idea/workspace.xml
@@ -26,4 +27,4 @@
.cxx
/.idea/deviceManager.xml
/.kotlin/
/.idea/AndroidProjectSystem.xml
/.idea/AndroidProjectSystem.xml

2
.idea/.gitignore generated vendored
View File

@@ -3,3 +3,5 @@
/workspace.xml
/migrations.xml
/runConfigurations.xml
/appInsightsSettings.xml
/kotlinCodeInsightSettings.xml

26
.idea/appInsightsSettings.xml generated Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -1,9 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
</value>
<value />
</option>
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
@@ -22,40 +20,46 @@
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="ALLOW_TRAILING_COMMA_COLLECTION_LITERAL_EXPRESSION" value="true" />
<option name="ALLOW_TRAILING_COMMA_VALUE_ARGUMENT_LIST" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Shell Script">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
@@ -64,7 +68,6 @@
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
<arrangement>
<rules>
@@ -179,9 +182,6 @@
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

4
.idea/gradle.xml generated
View File

@@ -6,7 +6,7 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-21" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
@@ -16,4 +16,4 @@
</GradleProjectSettings>
</option>
</component>
</project>
</project>

2
.idea/vcs.xml generated
View File

@@ -10,6 +10,6 @@
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,41 +1,33 @@
> [!IMPORTANT]
> In light of recent challenges — including threating actions from Kakao Entertainment Corp and upcoming Googles
> [new sideloading policy](https://f-droid.org/ru/2025/10/28/sideloading.html) — weve made the difficult decision to shut down Kotatsu and end its support. Were deeply grateful
> to everyone who contributed and to the amazing community that grew around this project.
---
<div align="center">
<a href="https://kotatsu.app">
<img src="./.github/assets/vtuber.png" alt="Kotatsu Logo" title="Kotatsu" width="600"/>
</a>
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in
online content sources.**
# [Kotatsu](https://kotatsu.app)
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in online content sources.**
![Downloads count](https://img.shields.io/github/downloads/KotatsuApp/Kotatsu/total?color=1976d2) ![Latest Stable version](https://img.shields.io/github/v/release/KotatsuApp/Kotatsu?color=2596be&label=latest) ![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) [![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF?)](https://t.me/kotatsuapp) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
### Download
<div align="left">
* **Recommended:** Download and install APK from [GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest). Application has a built-in self-updating feature.
* Get it on [F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu). The F-Droid build may be a bit outdated and some fixes might be missing.
* Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
</div>
![Android 6.0](https://img.shields.io/badge/android-6.0+-brightgreen) [![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF?)](https://t.me/kotatsuapp) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
### Main Features
<div align="left">
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers) (with 1100+ manga sources)
* Search manga by name, genres, and more filters
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers) (with 1200+ manga sources)
* Search manga by name, genres and more filters
* Favorites organized by user-defined categories
* Reading history, bookmarks, and incognito mode support
* Reading history, bookmarks and incognito mode support
* Download manga and read it offline. Third-party CBZ archives are also supported
* Clean and convenient Material You UI, optimized for phones, tablets, and desktop
* Clean and convenient Material You UI, optimized for phones, tablets and desktop
* Standard and Webtoon-optimized customizable reader, gesture support on reading interface
* Notifications about new chapters with updates feed, manga recommendations (with filters)
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password / fingerprint-protected access to the app
* Automatically sync app data with other devices on the same account
* Support for older devices running Android 5+
* Support for older devices running Android 6.0+
</div>
@@ -86,7 +78,18 @@ please head over to the [Weblate project page](https://hosted.weblate.org/engage
</br>
**📌 Pull requests are welcome, if you want: See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines**
**📌 Pull requests are welcome, if you want:
See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines**
### Certificate fingerprints
```plaintext
2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE
```
```plaintext
67:E1:51:00:BB:80:93:01:78:3E:DC:B6:34:8F:A3:BB:F8:30:34:D9:1E:62:86:8A:91:05:3D:BD:70:DB:3F:18
```
### License
@@ -94,7 +97,9 @@ please head over to the [Weblate project page](https://hosted.weblate.org/engage
<div align="left">
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
install instructions.
</div>
@@ -102,6 +107,9 @@ You may copy, distribute and modify the software as long as you track changes/da
<div align="left">
The developers of this application do not have any affiliation with the content available in the app. It collects content from sources that are freely available through any web browser.
The developers of this application do not have any affiliation with the content available in the app and does not store
or distribute any content. This application should be considered a web browser, all content that can be found using this
application is freely available on the Internet. All DMCA takedown requests should be sent to the owners of the website
where the content is hosted.
</div>

View File

@@ -3,96 +3,41 @@ import java.time.LocalDateTime
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
id 'androidx.room'
id 'org.jetbrains.kotlin.plugin.serialization'
// enable if needed
// id 'dev.reformator.stacktracedecoroutinator'
}
android {
compileSdk = 35
compileSdk = 36
buildToolsVersion = '35.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = 1012
versionName = '8.1.6'
minSdk = 23
targetSdk = 36
versionCode = 1033
versionName = '9.4.1'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
arg('room.generateKotlin', 'true')
}
androidResources {
// https://issuetracker.google.com/issues/408030127
generateLocaleConfig false
}
resourceConfigurations += [
"en",
"ab",
"ar",
"arq",
"as",
"be",
"bn",
"ca",
"cs",
"de",
"el",
"en-rGB",
"enm",
"es",
"et",
"eu",
"fa",
"fi",
"fil",
"fr",
"frp",
"gu",
"hi",
"hr",
"hu",
"in",
"it",
"iw",
"ja",
"kk",
"km",
"ko",
"lt",
"lv",
"lzh",
"ml",
"ms",
"my",
"nb-rNO",
"ne",
"nn",
"or",
"pa",
"pa-rPK",
"pl",
"pt",
"pt-rBR",
"ro",
"ru",
"si",
"sr",
"sv",
"ta",
"th",
"tr",
"uk",
"vi",
"zh-rCN",
"zh-rTW",
// Specific BCP 47 locales
"b+zh+Hans+MO",
"b+zh+Hant+MO"
]
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localProperties.load(new FileInputStream(localPropertiesFile))
}
resValue 'string', 'tg_backup_bot_token', localProperties.getProperty('tg_backup_bot_token', '')
}
buildTypes {
debug {
@@ -135,12 +80,15 @@ android {
'-opt-in=kotlin.ExperimentalStdlibApi',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi',
'-opt-in=kotlinx.coroutines.InternalForInheritanceCoroutinesApi',
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil3.annotation.ExperimentalCoilApi',
'-opt-in=coil3.annotation.InternalCoilApi',
'-opt-in=kotlinx.serialization.ExperimentalSerializationApi',
'-Xjspecify-annotations=strict',
'-Xtype-enhancement-improvements-strict-mode',
'-Xannotation-default-target=first-only',
'-Xtype-enhancement-improvements-strict-mode'
]
}
room {
@@ -167,13 +115,6 @@ android {
}
}
}
afterEvaluate {
compileDebugKotlin {
kotlinOptions {
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
}
}
}
dependencies {
def parsersVersion = libs.versions.parsers.get()
if (System.properties.containsKey('parsersVersionOverride')) {
@@ -201,6 +142,7 @@ dependencies {
implementation libs.lifecycle.service
implementation libs.lifecycle.process
implementation libs.androidx.constraintlayout
implementation libs.androidx.documentfile
implementation libs.androidx.swiperefreshlayout
implementation libs.androidx.recyclerview
implementation libs.androidx.viewpager2
@@ -213,6 +155,9 @@ dependencies {
implementation libs.androidx.work.runtime
implementation libs.guava
// Foldable/Window layout
implementation libs.androidx.window
implementation libs.androidx.room.runtime
implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler
@@ -221,14 +166,15 @@ dependencies {
implementation libs.okhttp.tls
implementation libs.okhttp.dnsoverhttps
implementation libs.okio
implementation libs.kotlinx.serialization.json
implementation libs.adapterdelegates
implementation libs.adapterdelegates.viewbinding
implementation libs.hilt.android
kapt libs.hilt.compiler
ksp libs.hilt.compiler
implementation libs.androidx.hilt.work
kapt libs.androidx.hilt.compiler
ksp libs.androidx.hilt.compiler
implementation libs.coil.core
implementation libs.coil.network
@@ -238,6 +184,7 @@ dependencies {
implementation libs.ssiv
implementation libs.disk.lru.cache
implementation libs.markwon
implementation libs.kizzyrpc
implementation libs.acra.http
implementation libs.acra.dialog
@@ -245,6 +192,7 @@ dependencies {
implementation libs.conscrypt.android
debugImplementation libs.leakcanary.android
nightlyImplementation libs.leakcanary.android
debugImplementation libs.workinspector
testImplementation libs.junit
@@ -262,5 +210,5 @@ dependencies {
androidTestImplementation libs.moshi.kotlin
androidTestImplementation libs.hilt.android.testing
kaptAndroidTest libs.hilt.android.compiler
kspAndroidTest libs.hilt.android.compiler
}

View File

@@ -8,8 +8,7 @@
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
@@ -17,10 +16,12 @@
-dontwarn com.google.j2objc.annotations.**
-dontwarn coil3.PlatformContext
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
-keep class org.koitharu.kotatsu.settings.about.changelog.ChangelogFragment
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
-keep class org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment { *; }
-keep class org.jsoup.parser.Tag
-keep class org.jsoup.internal.StringUtil

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,13 +30,15 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 1552943969433540704,
"name": "1 - 1",
"title": "1 - 1",
"number": 1,
"volume": 0,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -43,8 +46,9 @@
},
{
"id": 1552943969433540705,
"name": "1 - 2",
"title": "1 - 2",
"number": 2,
"volume": 0,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -52,8 +56,9 @@
},
{
"id": 1552943969433540706,
"name": "1 - 3",
"title": "1 - 3",
"number": 3,
"volume": 0,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -61,8 +66,9 @@
},
{
"id": 1552943969433540707,
"name": "1 - 4",
"title": "1 - 4",
"number": 4,
"volume": 0,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -70,8 +76,9 @@
},
{
"id": 1552943969433540708,
"name": "1 - 5",
"title": "1 - 5",
"number": 5,
"volume": 0,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -79,8 +86,9 @@
},
{
"id": 1552943969433541665,
"name": "2 - 1",
"title": "2 - 1",
"number": 6,
"volume": 0,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
@@ -88,8 +96,9 @@
},
{
"id": 1552943969433541666,
"name": "2 - 2",
"title": "2 - 2",
"number": 7,
"volume": 0,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
@@ -97,8 +106,9 @@
},
{
"id": 1552943969433541667,
"name": "2 - 3",
"title": "2 - 3",
"number": 8,
"volume": 0,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
@@ -106,8 +116,9 @@
},
{
"id": 1552943969433541668,
"name": "2 - 4",
"title": "2 - 4",
"number": 9,
"volume": 0,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
@@ -115,8 +126,9 @@
},
{
"id": 1552943969433541669,
"name": "2 - 5",
"title": "2 - 5",
"number": 10,
"volume": 0,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
@@ -124,8 +136,9 @@
},
{
"id": 1552943969433541670,
"name": "2 - 6",
"title": "2 - 6",
"number": 11,
"volume": 0,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
@@ -133,8 +146,9 @@
},
{
"id": 1552943969433542626,
"name": "3 - 1",
"title": "3 - 1",
"number": 12,
"volume": 0,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -142,8 +156,9 @@
},
{
"id": 1552943969433542627,
"name": "3 - 2",
"title": "3 - 2",
"number": 13,
"volume": 0,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -151,8 +166,9 @@
},
{
"id": 1552943969433542628,
"name": "3 - 3",
"title": "3 - 3",
"number": 14,
"volume": 0,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
@@ -160,4 +176,4 @@
}
],
"source": "READMANGA_RU"
}
}

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,8 +30,9 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [],
"source": "READMANGA_RU"
}
}

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,13 +30,15 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"title": "1 - 1",
"number": 1,
"volume": 0,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -43,8 +46,9 @@
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"title": "1 - 2",
"number": 2,
"volume": 0,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -52,8 +56,9 @@
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"title": "1 - 3",
"number": 3,
"volume": 0,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -61,8 +66,9 @@
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"title": "1 - 4",
"number": 4,
"volume": 0,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -70,8 +76,9 @@
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"title": "1 - 5",
"number": 5,
"volume": 0,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -79,8 +86,9 @@
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"title": "2 - 1",
"number": 6,
"volume": 0,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
@@ -88,8 +96,9 @@
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"title": "2 - 2",
"number": 7,
"volume": 0,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
@@ -97,8 +106,9 @@
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"title": "2 - 3",
"number": 8,
"volume": 0,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
@@ -106,8 +116,9 @@
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"title": "2 - 4",
"number": 9,
"volume": 0,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
@@ -115,8 +126,9 @@
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"title": "2 - 5",
"number": 10,
"volume": 0,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
@@ -124,8 +136,9 @@
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"title": "2 - 6",
"number": 11,
"volume": 0,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
@@ -133,4 +146,4 @@
}
],
"source": "READMANGA_RU"
}
}

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,13 +30,15 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"title": "1 - 1",
"number": 1,
"volume": 0,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -43,8 +46,9 @@
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"title": "1 - 2",
"number": 2,
"volume": 0,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -52,8 +56,9 @@
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"title": "1 - 3",
"number": 3,
"volume": 0,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -61,8 +66,9 @@
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"title": "1 - 4",
"number": 4,
"volume": 0,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -70,8 +76,9 @@
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"title": "1 - 5",
"number": 5,
"volume": 0,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -79,8 +86,9 @@
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"title": "2 - 1",
"number": 6,
"volume": 0,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
@@ -88,8 +96,9 @@
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"title": "2 - 2",
"number": 7,
"volume": 0,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
@@ -97,8 +106,9 @@
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"title": "2 - 3",
"number": 8,
"volume": 0,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
@@ -106,8 +116,9 @@
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"title": "2 - 4",
"number": 9,
"volume": 0,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
@@ -115,8 +126,9 @@
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"title": "2 - 5",
"number": 10,
"volume": 0,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
@@ -124,8 +136,9 @@
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"title": "2 - 6",
"number": 11,
"volume": 0,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
@@ -133,8 +146,9 @@
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"title": "3 - 1",
"number": 12,
"volume": 0,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -142,8 +156,9 @@
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"title": "3 - 2",
"number": 13,
"volume": 0,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -151,8 +166,9 @@
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"title": "3 - 3",
"number": 14,
"volume": 0,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
@@ -160,4 +176,4 @@
}
],
"source": "READMANGA_RU"
}
}

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,7 +30,8 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": null,
"source": "READMANGA_RU"
}
}

View File

@@ -1,6 +1,7 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"altTitles": [],
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
@@ -29,13 +30,15 @@
}
],
"state": "FINISHED",
"authors": [],
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
"chapters": [
{
"id": 3552943969433540704,
"name": "1 - 1",
"title": "1 - 1",
"number": 1,
"volume": 0,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -43,8 +46,9 @@
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"title": "1 - 2",
"number": 2,
"volume": 0,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -52,8 +56,9 @@
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"title": "1 - 3",
"number": 3,
"volume": 0,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -61,8 +66,9 @@
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"title": "1 - 4",
"number": 4,
"volume": 0,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -70,8 +76,9 @@
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"title": "1 - 5",
"number": 5,
"volume": 0,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
@@ -79,8 +86,9 @@
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"title": "2 - 2",
"number": 7,
"volume": 0,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
@@ -88,8 +96,9 @@
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"title": "2 - 3",
"number": 8,
"volume": 0,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
@@ -97,8 +106,9 @@
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"title": "2 - 4",
"number": 9,
"volume": 0,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
@@ -106,8 +116,9 @@
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"title": "2 - 5",
"number": 10,
"volume": 0,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
@@ -115,8 +126,9 @@
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"title": "2 - 6",
"number": 11,
"volume": 0,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
@@ -124,8 +136,9 @@
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"title": "3 - 1",
"number": 12,
"volume": 0,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -133,8 +146,9 @@
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"title": "3 - 2",
"number": 13,
"volume": 0,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
@@ -142,8 +156,9 @@
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"title": "3 - 3",
"number": 14,
"volume": 0,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
@@ -151,4 +166,4 @@
}
],
"source": "READMANGA_RU"
}
}

View File

@@ -1,19 +1,29 @@
package org.koitharu.kotatsu
import androidx.test.platform.app.InstrumentationRegistry
import com.squareup.moshi.*
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.buffer
import okio.source
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.time.Instant
import java.util.Date
import kotlin.reflect.KClass
object SampleData {
private val moshi = Moshi.Builder()
.add(DateAdapter())
.add(InstantAdapter())
.add(MangaSourceAdapter())
.add(KotlinJsonAdapterFactory())
.build()
@@ -51,4 +61,36 @@ object SampleData {
writer.value(value?.time ?: 0L)
}
}
}
private class MangaSourceAdapter : JsonAdapter<MangaSource>() {
@FromJson
override fun fromJson(reader: JsonReader): MangaSource? {
val name = reader.nextString() ?: return null
return MangaSource(name)
}
@ToJson
override fun toJson(writer: JsonWriter, value: MangaSource?) {
writer.value(value?.name)
}
}
private class InstantAdapter : JsonAdapter<Instant>() {
@FromJson
override fun fromJson(reader: JsonReader): Instant? {
val ms = reader.nextLong()
return if (ms == 0L) {
null
} else {
Instant.ofEpochMilli(ms)
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: Instant?) {
writer.value(value?.toEpochMilli() ?: 0L)
}
}
}

View File

@@ -15,7 +15,8 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.domain.AppBackupAgent
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository

View File

@@ -8,10 +8,6 @@ import androidx.core.content.edit
import androidx.fragment.app.strictmode.FragmentStrictMode
import leakcanary.LeakCanary
import org.koitharu.kotatsu.core.BaseApp
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
class KotatsuApp : BaseApp() {
@@ -45,8 +41,8 @@ class KotatsuApp : BaseApp() {
detectNetwork()
detectDiskWrites()
detectCustomSlowCalls()
detectResourceMismatches()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc()
penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
@@ -60,12 +56,10 @@ class KotatsuApp : BaseApp() {
detectLeakedSqlLiteObjects()
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectContentUriWithoutPermission()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
detectContentUriWithoutPermission()
}
detectFileUriExposure()
setClassInstanceLimit(LocalMangaRepository::class.java, 1)
setClassInstanceLimit(PagesCache::class.java, 1)
setClassInstanceLimit(MangaLoaderContext::class.java, 1)
setClassInstanceLimit(PageLoader::class.java, 1)
penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
penaltyListener(notifier.executor, notifier)

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.model.TestMangaSource
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.EnumSet
/*
This class is for parser development and testing purposes
You can open it in the app via Settings -> Debug
*/
class TestMangaRepository(
@Suppress("unused") private val loaderContext: MangaLoaderContext,
cache: MemoryContentCache
) : CachingMangaRepository(cache) {
override val source = TestMangaSource
override val sortOrders: Set<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override var defaultSortOrder: SortOrder
get() = sortOrders.first()
set(value) = Unit
override val filterCapabilities = MangaListFilterCapabilities()
override suspend fun getFilterOptions() = MangaListFilterOptions()
override suspend fun getList(
offset: Int,
order: SortOrder?,
filter: MangaListFilter?
): List<Manga> = TODO("Get manga list by filter")
override suspend fun getDetailsImpl(
manga: Manga
): Manga = TODO("Fetch manga details")
override suspend fun getPagesImpl(
chapter: MangaChapter
): List<MangaPage> = TODO("Get pages for specific chapter")
override suspend fun getPageUrl(
page: MangaPage
): String = TODO("Return direct url of page image or page.url if it is already a direct url")
override suspend fun getRelatedMangaImpl(
seed: Manga
): List<Manga> = TODO("Get list of related manga. This method is optional and parser library has a default implementation")
}

View File

@@ -0,0 +1,72 @@
package org.koitharu.kotatsu.settings
import android.os.Bundle
import androidx.preference.Preference
import leakcanary.LeakCanary
import org.koitharu.kotatsu.KotatsuApp
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TestMangaSource
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
import org.koitharu.workinspector.WorkInspector
class DebugSettingsFragment : BasePreferenceFragment(R.string.debug), Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener {
private val application
get() = requireContext().applicationContext as KotatsuApp
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_debug)
findPreference<SplitSwitchPreference>(KEY_LEAK_CANARY)?.let { pref ->
pref.isChecked = application.isLeakCanaryEnabled
pref.onPreferenceChangeListener = this
pref.onContainerClickListener = this
}
}
override fun onResume() {
super.onResume()
findPreference<SplitSwitchPreference>(KEY_LEAK_CANARY)?.isChecked = application.isLeakCanaryEnabled
}
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
KEY_WORK_INSPECTOR -> {
startActivity(WorkInspector.getIntent(preference.context))
true
}
KEY_TEST_PARSER -> {
router.openList(TestMangaSource, null, null)
true
}
else -> super.onPreferenceTreeClick(preference)
}
override fun onPreferenceClick(preference: Preference): Boolean = when (preference.key) {
KEY_LEAK_CANARY -> {
startActivity(LeakCanary.newLeakDisplayActivityIntent())
true
}
else -> super.onPreferenceTreeClick(preference)
}
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean = when (preference.key) {
KEY_LEAK_CANARY -> {
application.isLeakCanaryEnabled = newValue as Boolean
true
}
else -> false
}
private companion object {
const val KEY_LEAK_CANARY = "leak_canary"
const val KEY_WORK_INSPECTOR = "work_inspector"
const val KEY_TEST_PARSER = "test_parser"
}
}

View File

@@ -1,58 +0,0 @@
package org.koitharu.kotatsu.settings
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import leakcanary.LeakCanary
import org.koitharu.kotatsu.KotatsuApp
import org.koitharu.kotatsu.R
import org.koitharu.workinspector.WorkInspector
class SettingsMenuProvider(
private val context: Context,
) : MenuProvider {
private val application: KotatsuApp
get() = context.applicationContext as KotatsuApp
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_settings, menu)
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_leakcanary).isChecked = application.isLeakCanaryEnabled
menu.findItem(R.id.action_ssiv_debug).isChecked = SubsamplingScaleImageView.isDebug
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_leaks -> {
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
true
}
R.id.action_works -> {
context.startActivity(WorkInspector.getIntent(context))
true
}
R.id.action_leakcanary -> {
val checked = !menuItem.isChecked
menuItem.isChecked = checked
application.isLeakCanaryEnabled = checked
true
}
R.id.action_ssiv_debug -> {
val checked = !menuItem.isChecked
menuItem.isChecked = checked
SubsamplingScaleImageView.isDebug = checked
true
}
else -> false
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M20,8H17.19C16.74,7.2 16.12,6.5 15.37,6L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.05,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6C7.87,6.5 7.26,7.21 6.81,8H4V10H6.09C6.03,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.03,15.67 6.09,16H4V18H6.81C8.47,20.87 12.14,21.84 15,20.18C15.91,19.66 16.67,18.9 17.19,18H20V16H17.91C17.97,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.97,10.33 17.91,10H20V8M16,15A4,4 0 0,1 12,19A4,4 0 0,1 8,15V11A4,4 0 0,1 12,7A4,4 0 0,1 16,11V15M14,10V12H10V10H14M10,14H14V16H10V14Z" />
</vector>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_ssiv_debug"
android:checkable="true"
android:title="SSIV debug"
app:showAsAction="never"
tools:ignore="HardcodedText" />
<item
android:id="@+id/action_leakcanary"
android:checkable="true"
android:title="LeakCanary"
app:showAsAction="never"
tools:ignore="HardcodedText" />
<item
android:id="@+id/action_leaks"
android:title="@string/leak_canary_display_activity_label"
app:showAsAction="never" />
<item
android:id="@+id/action_works"
android:title="@string/wi_lib_name"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
android:key="leak_canary"
android:persistent="false"
android:title="LeakCanary" />
<Preference
android:key="work_inspector"
android:persistent="false"
android:title="@string/wi_lib_name" />
<Preference
android:key="test_parser"
android:persistent="false"
android:title="@string/test_parser"
app:allowDividerAbove="true" />
</androidx.preference.PreferenceScreen>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.DebugSettingsFragment"
android:icon="@drawable/ic_debug"
android:key="debug"
android:title="@string/debug" />
</androidx.preference.PreferenceScreen>

View File

@@ -5,7 +5,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -19,17 +21,19 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:ignore="RequestInstallPackagesPolicy" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
tools:ignore="AllFilesAccessPolicy,ScopedStorage" />
<queries>
<intent>
@@ -44,16 +48,18 @@
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
android:allowBackup="true"
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
android:backupAgent="org.koitharu.kotatsu.backups.domain.AppBackupAgent"
android:dataExtractionRules="@xml/backup_rules"
android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
android:extractNativeLibs="true"
android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true"
android:hasFragileUserData="true"
android:restoreAnyVersion="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
@@ -209,6 +215,9 @@
android:launchMode="singleTop" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
<activity
android:name="org.koitharu.kotatsu.settings.override.OverrideConfigActivity"
android:label="@string/edit" />
<activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:exported="true"
@@ -262,6 +271,27 @@
<activity
android:name="org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity"
android:label="@string/tracker_debug_info" />
<activity
android:name="org.koitharu.kotatsu.picker.ui.PageImagePickActivity"
android:exported="true"
android:label="@string/pick_manga_page">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PICK" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.scrobbling.discord.ui.DiscordAuthActivity"
android:label="@string/discord" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
@@ -272,7 +302,7 @@
android:foregroundServiceType="dataSync"
android:label="@string/local_manga_processing" />
<service
android:name="org.koitharu.kotatsu.settings.backup.PeriodicalBackupService"
android:name="org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupService"
android:foregroundServiceType="dataSync"
android:label="@string/periodic_backups" />
<service
@@ -283,9 +313,13 @@
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
android:label="@string/local_manga_processing" />
<service
android:name="org.koitharu.kotatsu.settings.backup.RestoreService"
android:name="org.koitharu.kotatsu.backups.ui.backup.BackupService"
android:foregroundServiceType="dataSync"
android:label="@string/restore_backup" />
android:label="@string/creating_backup" />
<service
android:name="org.koitharu.kotatsu.backups.ui.restore.RestoreService"
android:foregroundServiceType="dataSync"
android:label="@string/restoring_backup" />
<service
android:name="org.koitharu.kotatsu.local.ui.ImportService"
android:foregroundServiceType="dataSync"
@@ -335,6 +369,9 @@
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
android:exported="false"
android:label="@string/prefetch_content" />
<service
android:name="org.koitharu.kotatsu.browser.AdListUpdateService"
android:exported="false" />
<provider
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
@@ -372,6 +409,13 @@
tools:node="remove" />
</provider>
<receiver
android:name="org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler$DiscardReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.koitharu.kotatsu.CAPTCHA_DISCARD" />
</intent-filter>
</receiver>
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"

View File

@@ -30,21 +30,19 @@ constructor(
oldManga: Manga,
newManga: Manga,
) {
val oldDetails =
if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails =
if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails)
val oldDetails = if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails = if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails, replaceExisting = true)
database.withTransaction {
// replace favorites
val favoritesDao = database.getFavouritesDao()
@@ -101,11 +99,11 @@ constructor(
mangaId = newDetails.id,
rating = prevInfo.rating,
status =
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
comment = prevInfo.comment,
)
if (newHistory != null) {

View File

@@ -21,16 +21,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemMangaAlternativeBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -104,13 +99,6 @@ fun alternativeAD(
.allowRgb565(true)
.enqueueWith(coil)
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
transformations(TrimTransformation())
allowRgb565(true)
mangaExtra(item.manga)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.manga.coverUrl, item.manga)
}
}

View File

@@ -51,7 +51,7 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
}
val listAdapter = BaseListAdapter<ListModel>()
.addDelegate(ListItemType.MANGA_LIST_DETAILED, alternativeAD(coil, this, this))
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, this, null))
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
.addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
.addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
.addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(this))

View File

@@ -17,8 +17,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase.NoAlternativesException
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
@@ -46,7 +48,7 @@ class AutoFixService : CoroutineIntentService() {
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager = NotificationManagerCompat.from(this)
}
override suspend fun IntentJobContext.processIntent(intent: Intent) {
@@ -57,8 +59,8 @@ class AutoFixService : CoroutineIntentService() {
val result = runCatchingCancellable {
autoFixUseCase.invoke(mangaId)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result)
if (checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(startId, result)
notificationManager.notify(TAG, startId, notification)
}
}
@@ -66,15 +68,15 @@ class AutoFixService : CoroutineIntentService() {
}
override fun IntentJobContext.onError(error: Throwable) {
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = runBlocking { buildNotification(Result.failure(error)) }
if (checkNotificationPermission(CHANNEL_ID)) {
val notification = runBlocking { buildNotification(startId, Result.failure(error)) }
notificationManager.notify(TAG, startId, notification)
}
}
@SuppressLint("InlinedApi")
private fun startForeground(jobContext: IntentJobContext) {
val title = applicationContext.getString(R.string.fixing_manga)
val title = getString(R.string.fixing_manga)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN)
.setName(title)
.setShowBadge(false)
@@ -84,7 +86,7 @@ class AutoFixService : CoroutineIntentService() {
.build()
notificationManager.createNotificationChannel(channel)
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setDefaults(0)
@@ -96,7 +98,7 @@ class AutoFixService : CoroutineIntentService() {
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
appcompatR.drawable.abc_ic_clear_material,
applicationContext.getString(android.R.string.cancel),
getString(android.R.string.cancel),
jobContext.getCancelIntent(),
)
.build()
@@ -108,8 +110,8 @@ class AutoFixService : CoroutineIntentService() {
)
}
private suspend fun buildNotification(result: Result<Pair<Manga, Manga?>>): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
private suspend fun buildNotification(startId: Int, result: Result<Pair<Manga, Manga?>>): Notification {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0)
.setSilent(true)
@@ -118,59 +120,64 @@ class AutoFixService : CoroutineIntentService() {
if (replacement != null) {
notification.setLargeIcon(
coil.execute(
ImageRequest.Builder(applicationContext)
ImageRequest.Builder(this)
.data(replacement.coverUrl)
.mangaSourceExtra(replacement.source)
.build(),
).toBitmapOrNull(),
)
notification.setSubText(replacement.title)
val intent = AppRouter.detailsIntent(applicationContext, replacement)
val intent = AppRouter.detailsIntent(this, replacement)
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
this,
replacement.id.toInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
false,
),
).setVisibility(
if (replacement.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC,
if (replacement.isNsfw()) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
)
notification
.setContentTitle(applicationContext.getString(R.string.fixed))
.setContentTitle(getString(R.string.fixed))
.setContentText(
applicationContext.getString(
getString(
R.string.manga_replaced,
seed.title,
seed.source.getTitle(applicationContext),
seed.source.getTitle(this),
replacement.title,
replacement.source.getTitle(applicationContext),
replacement.source.getTitle(this),
),
)
.setSmallIcon(R.drawable.ic_stat_done)
} else {
notification
.setContentTitle(applicationContext.getString(R.string.fixing_manga))
.setContentText(applicationContext.getString(R.string.no_fix_required, seed.title))
.setContentTitle(getString(R.string.fixing_manga))
.setContentText(getString(R.string.no_fix_required, seed.title))
.setSmallIcon(android.R.drawable.stat_sys_warning)
}
}.onFailure { error ->
notification
.setContentTitle(applicationContext.getString(R.string.error_occurred))
.setContentTitle(getString(R.string.error_occurred))
.setContentText(
if (error is AutoFixUseCase.NoAlternativesException) {
applicationContext.getString(R.string.no_alternatives_found, error.seed.manga.title)
if (error is NoAlternativesException) {
getString(R.string.no_alternatives_found, error.seed.manga.title)
} else {
error.getDisplayMessage(applicationContext.resources)
error.getDisplayMessage(resources)
},
).setSmallIcon(android.R.drawable.stat_notify_error)
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent ->
notification.addAction(
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
reportIntent,
)
ErrorReporterReceiver.getNotificationAction(
context = this,
e = error,
notificationId = startId,
notificationTag = TAG,
)?.let { action ->
notification.addAction(action)
}
}
return notification.build()

View File

@@ -0,0 +1,314 @@
package org.koitharu.kotatsu.backups.data
import androidx.collection.ArrayMap
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.DecodeSequenceMode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeToSequence
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.serializer
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.backups.data.model.BackupIndex
import org.koitharu.kotatsu.backups.data.model.BookmarkBackup
import org.koitharu.kotatsu.backups.data.model.CategoryBackup
import org.koitharu.kotatsu.backups.data.model.FavouriteBackup
import org.koitharu.kotatsu.backups.data.model.HistoryBackup
import org.koitharu.kotatsu.backups.data.model.MangaBackup
import org.koitharu.kotatsu.backups.data.model.ScrobblingBackup
import org.koitharu.kotatsu.backups.data.model.SourceBackup
import org.koitharu.kotatsu.backups.data.model.StatisticBackup
import org.koitharu.kotatsu.backups.domain.BackupSection
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.data.PersistableFilter
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import javax.inject.Inject
@Reusable
class BackupRepository @Inject constructor(
private val database: MangaDatabase,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
private val mangaSourcesRepository: MangaSourcesRepository,
private val savedFiltersRepository: SavedFiltersRepository,
) {
private val json = Json {
allowSpecialFloatingPointValues = true
coerceInputValues = true
encodeDefaults = true
ignoreUnknownKeys = true
useAlternativeNames = false
}
suspend fun createBackup(
output: ZipOutputStream,
progress: FlowCollector<Progress>?,
) {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, BackupSection.entries.size)
for (section in BackupSection.entries) {
when (section) {
BackupSection.INDEX -> output.writeJsonArray(
section = BackupSection.INDEX,
data = flowOf(BackupIndex()),
serializer = serializer(),
)
BackupSection.HISTORY -> output.writeJsonArray(
section = BackupSection.HISTORY,
data = database.getHistoryDao().dump().map { HistoryBackup(it) },
serializer = serializer(),
)
BackupSection.CATEGORIES -> output.writeJsonArray(
section = BackupSection.CATEGORIES,
data = database.getFavouriteCategoriesDao().findAll().asFlow().map { CategoryBackup(it) },
serializer = serializer(),
)
BackupSection.FAVOURITES -> output.writeJsonArray(
section = BackupSection.FAVOURITES,
data = database.getFavouritesDao().dump().map { FavouriteBackup(it) },
serializer = serializer(),
)
BackupSection.SETTINGS -> output.writeString(
section = BackupSection.SETTINGS,
data = dumpSettings(),
)
BackupSection.SETTINGS_READER_GRID -> output.writeString(
section = BackupSection.SETTINGS_READER_GRID,
data = dumpReaderGridSettings(),
)
BackupSection.BOOKMARKS -> output.writeJsonArray(
section = BackupSection.BOOKMARKS,
data = database.getBookmarksDao().dump().map { BookmarkBackup(it.first, it.second) },
serializer = serializer(),
)
BackupSection.SOURCES -> output.writeJsonArray(
section = BackupSection.SOURCES,
data = database.getSourcesDao().dumpEnabled().map { SourceBackup(it) },
serializer = serializer(),
)
BackupSection.SCROBBLING -> output.writeJsonArray(
section = BackupSection.SCROBBLING,
data = database.getScrobblingDao().dumpEnabled().map { ScrobblingBackup(it) },
serializer = serializer(),
)
BackupSection.STATS -> output.writeJsonArray(
section = BackupSection.STATS,
data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) },
serializer = serializer(),
)
BackupSection.SAVED_FILTERS -> {
val sources = mangaSourcesRepository.getEnabledSources()
val filters = sources.flatMap { source ->
savedFiltersRepository.getAll(source)
}
output.writeJsonArray(
section = BackupSection.SAVED_FILTERS,
data = filters.asFlow(),
serializer = serializer(),
)
}
}
progress?.emit(commonProgress)
commonProgress++
}
progress?.emit(commonProgress)
}
suspend fun restoreBackup(
input: ZipInputStream,
sections: Set<BackupSection>,
progress: FlowCollector<Progress>?,
): CompositeResult {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, sections.size)
var entry = input.nextEntry
var result = CompositeResult.EMPTY
while (entry != null) {
val section = BackupSection.of(entry)
if (section in sections) {
result += when (section) {
BackupSection.INDEX -> CompositeResult.EMPTY // useless in our case
BackupSection.HISTORY -> input.readJsonArray<HistoryBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getHistoryDao().upsert(it.toEntity())
}
BackupSection.CATEGORIES -> input.readJsonArray<CategoryBackup>(serializer()).restoreToDb {
getFavouriteCategoriesDao().upsert(it.toEntity())
}
BackupSection.FAVOURITES -> input.readJsonArray<FavouriteBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getFavouritesDao().upsert(it.toEntity())
}
BackupSection.SETTINGS -> input.readMap().let {
settings.upsertAll(it)
CompositeResult.success()
}
BackupSection.SETTINGS_READER_GRID -> input.readMap().let {
tapGridSettings.upsertAll(it)
CompositeResult.success()
}
BackupSection.BOOKMARKS -> input.readJsonArray<BookmarkBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getBookmarksDao().upsert(it.bookmarks.map { b -> b.toEntity() })
}
BackupSection.SOURCES -> input.readJsonArray<SourceBackup>(serializer()).restoreToDb {
getSourcesDao().upsert(it.toEntity())
}
BackupSection.SCROBBLING -> input.readJsonArray<ScrobblingBackup>(serializer()).restoreToDb {
getScrobblingDao().upsert(it.toEntity())
}
BackupSection.STATS -> input.readJsonArray<StatisticBackup>(serializer()).restoreToDb {
getStatsDao().upsert(it.toEntity())
}
BackupSection.SAVED_FILTERS -> input.readJsonArray<PersistableFilter>(serializer())
.restoreWithoutTransaction {
savedFiltersRepository.save(it)
}
null -> CompositeResult.EMPTY // skip unknown entries
}
progress?.emit(commonProgress)
commonProgress++
}
input.closeEntry()
entry = input.nextEntry
}
progress?.emit(commonProgress)
return result
}
private suspend fun <T> ZipOutputStream.writeJsonArray(
section: BackupSection,
data: Flow<T>,
serializer: SerializationStrategy<T>,
) {
data.onStart {
putNextEntry(ZipEntry(section.entryName))
write("[")
}.onCompletion { error ->
if (error == null) {
write("]")
}
closeEntry()
flush()
}.collectIndexed { index, value ->
if (index > 0) {
write(",")
}
json.encodeToStream(serializer, value, this)
}
}
private fun <T> InputStream.readJsonArray(
serializer: DeserializationStrategy<T>,
): Sequence<T> = json.decodeToSequence(this, serializer, DecodeSequenceMode.ARRAY_WRAPPED)
private fun InputStream.readMap(): Map<String, Any?> {
val jo = JSONArray(readString()).getJSONObject(0)
val map = ArrayMap<String, Any?>(jo.length())
val keys = jo.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = jo.get(key)
}
return map
}
private fun ZipOutputStream.writeString(
section: BackupSection,
data: String,
) {
putNextEntry(ZipEntry(section.entryName))
try {
write("[")
write(data)
write("]")
} finally {
closeEntry()
flush()
}
}
private fun OutputStream.write(str: String) = write(str.toByteArray())
private fun InputStream.readString(): String = readBytes().decodeToString()
private fun dumpSettings(): String {
val map = settings.getAllValues().toMutableMap()
map.remove(AppSettings.KEY_APP_PASSWORD)
map.remove(AppSettings.KEY_PROXY_PASSWORD)
map.remove(AppSettings.KEY_PROXY_LOGIN)
map.remove(AppSettings.KEY_INCOGNITO_MODE)
return JSONObject(map).toString()
}
private fun dumpReaderGridSettings(): String {
return JSONObject(tapGridSettings.getAllValues()).toString()
}
private suspend fun MangaDatabase.upsertManga(manga: MangaBackup) {
val tags = manga.tags.map { it.toEntity() }
getTagsDao().upsert(tags)
getMangaDao().upsert(manga.toEntity(), tags)
}
private suspend inline fun <T> Sequence<T>.restoreToDb(crossinline block: suspend MangaDatabase.(T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
database.withTransaction {
database.block(item)
}
}
}
}
private suspend inline fun <T> Sequence<T>.restoreWithoutTransaction(crossinline block: suspend (T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
block(item)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.BuildConfig
@Serializable
class BackupIndex(
@SerialName("app_id") val appId: String,
@SerialName("app_version") val appVersion: Int,
@SerialName("created_at") val createdAt: Long,
) {
constructor() : this(
appId = BuildConfig.APPLICATION_ID,
appVersion = BuildConfig.VERSION_CODE,
createdAt = System.currentTimeMillis(),
)
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
import org.koitharu.kotatsu.parsers.util.mapToSet
@Serializable
class BookmarkBackup(
@SerialName("manga") val manga: MangaBackup,
@SerialName("tags") val tags: Set<TagBackup>,
@SerialName("bookmarks") val bookmarks: List<Bookmark>,
) {
@Serializable
class Bookmark(
@SerialName("manga_id") val mangaId: Long,
@SerialName("page_id") val pageId: Long,
@SerialName("chapter_id") val chapterId: Long,
@SerialName("page") val page: Int,
@SerialName("scroll") val scroll: Int,
@SerialName("image_url") val imageUrl: String,
@SerialName("created_at") val createdAt: Long,
@SerialName("percent") val percent: Float,
) {
fun toEntity() = BookmarkEntity(
mangaId = mangaId,
pageId = pageId,
chapterId = chapterId,
page = page,
scroll = scroll,
imageUrl = imageUrl,
createdAt = createdAt,
percent = percent,
)
}
constructor(manga: MangaWithTags, entities: List<BookmarkEntity>) : this(
manga = MangaBackup(manga.copy(tags = emptyList())),
tags = manga.tags.mapToSet { TagBackup(it) },
bookmarks = entities.map {
Bookmark(
mangaId = it.mangaId,
pageId = it.pageId,
chapterId = it.chapterId,
page = it.page,
scroll = it.scroll,
imageUrl = it.imageUrl,
createdAt = it.createdAt,
percent = it.percent,
)
},
)
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.list.domain.ListSortOrder
@Serializable
class CategoryBackup(
@SerialName("category_id") val categoryId: Int,
@SerialName("created_at") val createdAt: Long,
@SerialName("sort_key") val sortKey: Int,
@SerialName("title") val title: String,
@SerialName("order") val order: String = ListSortOrder.NEWEST.name,
@SerialName("track") val track: Boolean = true,
@SerialName("show_in_lib") val isVisibleInLibrary: Boolean = true,
) {
constructor(entity: FavouriteCategoryEntity) : this(
categoryId = entity.categoryId,
createdAt = entity.createdAt,
sortKey = entity.sortKey,
title = entity.title,
order = entity.order,
track = entity.track,
isVisibleInLibrary = entity.isVisibleInLibrary,
)
fun toEntity() = FavouriteCategoryEntity(
categoryId = categoryId,
createdAt = createdAt,
sortKey = sortKey,
title = title,
order = order,
track = track,
isVisibleInLibrary = isVisibleInLibrary,
deletedAt = 0L,
)
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouriteManga
@Serializable
class FavouriteBackup(
@SerialName("manga_id") val mangaId: Long,
@SerialName("category_id") val categoryId: Long,
@SerialName("sort_key") val sortKey: Int = 0,
@SerialName("pinned") val isPinned: Boolean = false,
@SerialName("created_at") val createdAt: Long,
@SerialName("manga") val manga: MangaBackup,
) {
constructor(entity: FavouriteManga) : this(
mangaId = entity.manga.id,
categoryId = entity.favourite.categoryId,
sortKey = entity.favourite.sortKey,
isPinned = entity.favourite.isPinned,
createdAt = entity.favourite.createdAt,
manga = MangaBackup(MangaWithTags(entity.manga, entity.tags)),
)
fun toEntity() = FavouriteEntity(
mangaId = mangaId,
categoryId = categoryId,
sortKey = sortKey,
isPinned = isPinned,
createdAt = createdAt,
deletedAt = 0L,
)
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.HistoryWithManga
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_NONE
@Serializable
class HistoryBackup(
@SerialName("manga_id") val mangaId: Long,
@SerialName("created_at") val createdAt: Long,
@SerialName("updated_at") val updatedAt: Long,
@SerialName("chapter_id") val chapterId: Long,
@SerialName("page") val page: Int,
@SerialName("scroll") val scroll: Float,
@SerialName("percent") val percent: Float = PROGRESS_NONE,
@SerialName("chapters") val chaptersCount: Int = 0,
@SerialName("manga") val manga: MangaBackup,
) {
constructor(entity: HistoryWithManga) : this(
mangaId = entity.manga.id,
createdAt = entity.history.createdAt,
updatedAt = entity.history.updatedAt,
chapterId = entity.history.chapterId,
page = entity.history.page,
scroll = entity.history.scroll,
percent = entity.history.percent,
chaptersCount = entity.history.chaptersCount,
manga = MangaBackup(MangaWithTags(entity.manga, entity.tags)),
)
fun toEntity() = HistoryEntity(
mangaId = mangaId,
createdAt = createdAt,
updatedAt = updatedAt,
chapterId = chapterId,
page = page,
scroll = scroll,
percent = percent,
deletedAt = 0L,
chaptersCount = chaptersCount,
)
}

View File

@@ -0,0 +1,60 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.parsers.util.mapToSet
@Serializable
class MangaBackup(
@SerialName("id") val id: Long,
@SerialName("title") val title: String,
@SerialName("alt_title") val altTitles: String? = null,
@SerialName("url") val url: String,
@SerialName("public_url") val publicUrl: String,
@SerialName("rating") val rating: Float = RATING_UNKNOWN,
@SerialName("nsfw") val isNsfw: Boolean = false,
@SerialName("content_rating") val contentRating: String? = null,
@SerialName("cover_url") val coverUrl: String,
@SerialName("large_cover_url") val largeCoverUrl: String? = null,
@SerialName("state") val state: String? = null,
@SerialName("author") val authors: String? = null,
@SerialName("source") val source: String,
@SerialName("tags") val tags: Set<TagBackup> = emptySet(),
) {
constructor(entity: MangaWithTags) : this(
id = entity.manga.id,
title = entity.manga.title,
altTitles = entity.manga.altTitles,
url = entity.manga.url,
publicUrl = entity.manga.publicUrl,
rating = entity.manga.rating,
isNsfw = entity.manga.isNsfw,
contentRating = entity.manga.contentRating,
coverUrl = entity.manga.coverUrl,
largeCoverUrl = entity.manga.largeCoverUrl,
state = entity.manga.state,
authors = entity.manga.authors,
source = entity.manga.source,
tags = entity.tags.mapToSet { TagBackup(it) },
)
fun toEntity() = MangaEntity(
id = id,
title = title,
altTitles = altTitles,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
contentRating = contentRating,
coverUrl = coverUrl,
largeCoverUrl = largeCoverUrl,
state = state,
authors = authors,
source = source,
)
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
@Serializable
class ScrobblingBackup(
@SerialName("scrobbler") val scrobbler: Int,
@SerialName("id") val id: Int,
@SerialName("manga_id") val mangaId: Long,
@SerialName("target_id") val targetId: Long,
@SerialName("status") val status: String?,
@SerialName("chapter") val chapter: Int,
@SerialName("comment") val comment: String?,
@SerialName("rating") val rating: Float,
) {
constructor(entity: ScrobblingEntity) : this(
scrobbler = entity.scrobbler,
id = entity.id,
mangaId = entity.mangaId,
targetId = entity.targetId,
status = entity.status,
chapter = entity.chapter,
comment = entity.comment,
rating = entity.rating,
)
fun toEntity() = ScrobblingEntity(
scrobbler = scrobbler,
id = id,
mangaId = mangaId,
targetId = targetId,
status = status,
chapter = chapter,
comment = comment,
rating = rating,
)
}

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
@Serializable
class SourceBackup(
@SerialName("source") val source: String,
@SerialName("sort_key") val sortKey: Int,
@SerialName("used_at") val lastUsedAt: Long,
@SerialName("added_in") val addedIn: Int,
@SerialName("pinned") val isPinned: Boolean = false,
@SerialName("enabled") val isEnabled: Boolean = true, // for compatibility purposes, should be only true
) {
constructor(entity: MangaSourceEntity) : this(
source = entity.source,
sortKey = entity.sortKey,
lastUsedAt = entity.lastUsedAt,
addedIn = entity.addedIn,
isPinned = entity.isPinned,
isEnabled = entity.isEnabled,
)
fun toEntity() = MangaSourceEntity(
source = source,
isEnabled = isEnabled,
sortKey = sortKey,
addedIn = addedIn,
lastUsedAt = lastUsedAt,
isPinned = isPinned,
cfState = 0,
)
}

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.stats.data.StatsEntity
@Serializable
class StatisticBackup(
@SerialName("manga_id") val mangaId: Long,
@SerialName("started_at") val startedAt: Long,
@SerialName("duration") val duration: Long,
@SerialName("pages") val pages: Int,
) {
constructor(entity: StatsEntity) : this(
mangaId = entity.mangaId,
startedAt = entity.startedAt,
duration = entity.duration,
pages = entity.pages,
)
fun toEntity() = StatsEntity(
mangaId = mangaId,
startedAt = startedAt,
duration = duration,
pages = pages,
)
}

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Serializable
class TagBackup(
@SerialName("id") val id: Long,
@SerialName("title") val title: String,
@SerialName("key") val key: String,
@SerialName("source") val source: String,
@SerialName("pinned") val isPinned: Boolean = false,
) {
constructor(entity: TagEntity) : this(
id = entity.id,
title = entity.title,
key = entity.key,
source = entity.source,
isPinned = entity.isPinned,
)
fun toEntity() = TagEntity(
id = id,
title = title,
key = key,
source = source,
isPinned = isPinned,
)
}

View File

@@ -0,0 +1,119 @@
package org.koitharu.kotatsu.backups.domain
import android.app.backup.BackupAgent
import android.app.backup.BackupDataInput
import android.app.backup.BackupDataOutput
import android.app.backup.FullBackupDataOutput
import android.content.Context
import android.os.ParcelFileDescriptor
import androidx.annotation.VisibleForTesting
import com.google.common.io.ByteStreams
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.io.File
import java.io.FileDescriptor
import java.io.FileInputStream
import java.util.EnumSet
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class AppBackupAgent : BackupAgent() {
override fun onBackup(
oldState: ParcelFileDescriptor?,
data: BackupDataOutput?,
newState: ParcelFileDescriptor?
) = Unit
override fun onRestore(
data: BackupDataInput?,
appVersionCode: Int,
newState: ParcelFileDescriptor?
) = Unit
override fun onFullBackup(data: FullBackupDataOutput) {
super.onFullBackup(data)
val file = createBackupFile(
this,
BackupRepository(
database = MangaDatabase(context = applicationContext),
settings = AppSettings(applicationContext),
tapGridSettings = TapGridSettings(applicationContext),
mangaSourcesRepository = MangaSourcesRepository(
context = applicationContext,
db = MangaDatabase(context = applicationContext),
settings = AppSettings(applicationContext),
),
savedFiltersRepository = SavedFiltersRepository(
context = applicationContext,
),
),
)
try {
fullBackupFile(file, data)
} finally {
file.delete()
}
}
override fun onRestoreFile(
data: ParcelFileDescriptor,
size: Long,
destination: File?,
type: Int,
mode: Long,
mtime: Long
) {
if (destination?.name?.endsWith(".bk.zip") == true) {
restoreBackupFile(
data.fileDescriptor,
size,
BackupRepository(
database = MangaDatabase(applicationContext),
settings = AppSettings(applicationContext),
tapGridSettings = TapGridSettings(applicationContext),
mangaSourcesRepository = MangaSourcesRepository(
context = applicationContext,
db = MangaDatabase(context = applicationContext),
settings = AppSettings(applicationContext),
),
savedFiltersRepository = SavedFiltersRepository(
context = applicationContext,
),
),
)
destination.delete()
} else {
super.onRestoreFile(data, size, destination, type, mode, mtime)
}
}
@VisibleForTesting
fun createBackupFile(context: Context, repository: BackupRepository): File {
val file = BackupUtils.createTempFile(context)
ZipOutputStream(file.outputStream()).use { output ->
runBlocking {
repository.createBackup(output, null)
}
}
return file
}
@VisibleForTesting
fun restoreBackupFile(fd: FileDescriptor, size: Long, repository: BackupRepository) {
ZipInputStream(ByteStreams.limit(FileInputStream(fd), size)).use { input ->
val sections = EnumSet.allOf(BackupSection::class.java)
// managed externally
sections.remove(BackupSection.SETTINGS)
sections.remove(BackupSection.SETTINGS_READER_GRID)
runBlocking {
repository.restoreBackup(input, sections, null)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.backup
package org.koitharu.kotatsu.backups.domain
import android.net.Uri
import java.util.Date
@@ -6,7 +6,7 @@ import java.util.Date
data class BackupFile(
val uri: Uri,
val dateTime: Date,
): Comparable<BackupFile> {
) : Comparable<BackupFile> {
override fun compareTo(other: BackupFile): Int = compareValues(dateTime, other.dateTime)
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.domain
import android.app.backup.BackupManager
import android.content.Context
@@ -13,7 +13,13 @@ import javax.inject.Singleton
@Singleton
class BackupObserver @Inject constructor(
@ApplicationContext context: Context,
) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
) : InvalidationTracker.Observer(
arrayOf(
TABLE_HISTORY,
TABLE_FAVOURITES,
TABLE_FAVOURITE_CATEGORIES,
),
) {
private val backupManager = BackupManager(context)

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.backups.domain
import java.util.Locale
import java.util.zip.ZipEntry
enum class BackupSection(
val entryName: String,
) {
INDEX("index"),
HISTORY("history"),
CATEGORIES("categories"),
FAVOURITES("favourites"),
SETTINGS("settings"),
SETTINGS_READER_GRID("reader_grid"),
BOOKMARKS("bookmarks"),
SOURCES("sources"),
SCROBBLING("scrobbling"),
STATS("statistics"),
SAVED_FILTERS("saved_filters"),
;
companion object {
fun of(entry: ZipEntry): BackupSection? {
val name = entry.name.lowercase(Locale.ROOT)
return entries.find { x -> x.entryName == name }
}
}
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.backups.domain
import android.content.Context
import androidx.annotation.CheckResult
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object BackupUtils {
private const val DIR_BACKUPS = "backups"
private val dateTimeFormat = SimpleDateFormat("yyyyMMdd-HHmm")
@CheckResult
fun createTempFile(context: Context): File {
val dir = getAppBackupDir(context)
dir.mkdirs()
return File(dir, generateFileName(context))
}
fun getAppBackupDir(context: Context) = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}
fun parseBackupDateTime(fileName: String): Date? = try {
dateTimeFormat.parse(fileName.substringAfterLast('_').substringBefore('.'))
} catch (e: ParseException) {
e.printStackTraceDebug()
null
}
fun generateFileName(context: Context) = buildString {
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
append('_')
append(dateTimeFormat.format(Date()))
append(".bk.zip")
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.backup
package org.koitharu.kotatsu.backups.domain
import android.content.Context
import android.net.Uri
@@ -28,7 +28,7 @@ class ExternalBackupStorage @Inject constructor(
BackupFile(
uri = it.uri,
dateTime = it.name?.let { fileName ->
BackupZipOutput.parseBackupDateTime(fileName)
BackupUtils.parseBackupDateTime(fileName)
} ?: return@mapNotNull null,
)
} else {
@@ -44,7 +44,12 @@ class ExternalBackupStorage @Inject constructor(
}.getOrNull()
suspend fun put(file: File): Uri = runInterruptible(Dispatchers.IO) {
val out = checkNotNull(getRootOrThrow().createFile("application/zip", file.nameWithoutExtension)) {
val out = checkNotNull(
getRootOrThrow().createFile(
"application/zip",
file.nameWithoutExtension,
),
) {
"Cannot create target backup file"
}
checkNotNull(context.contentResolver.openOutputStream(out.uri, "wt")).sink().use { sink ->

View File

@@ -0,0 +1,136 @@
package org.koitharu.kotatsu.backups.ui
import android.content.Context
import android.net.Uri
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ShareCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getFileDisplayName
import androidx.appcompat.R as appcompatR
abstract class BaseBackupRestoreService : CoroutineIntentService() {
protected abstract val notificationTag: String
protected abstract val isRestoreService: Boolean
protected lateinit var notificationManager: NotificationManagerCompat
private set
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(applicationContext)
createNotificationChannel(this)
}
override fun IntentJobContext.onError(error: Throwable) {
showResultNotification(null, CompositeResult.failure(error))
}
protected fun IntentJobContext.showResultNotification(
fileUri: Uri?,
result: CompositeResult,
) {
if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) {
return
}
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setAutoCancel(true)
.setSubText(fileUri?.let { contentResolver.getFileDisplayName(it) })
when {
result.isAllSuccess -> {
if (isRestoreService) {
notification
.setContentTitle(getString(R.string.restoring_backup))
.setContentText(getString(R.string.data_restored_success))
} else {
notification
.setContentTitle(getString(R.string.backup_saved))
.setContentText(fileUri?.let { contentResolver.getFileDisplayName(it) })
.setSubText(null)
}
notification.setSmallIcon(R.drawable.ic_stat_done)
}
result.isAllFailed || !isRestoreService -> {
val title = getString(if (isRestoreService) R.string.data_not_restored else R.string.error_occurred)
val message = result.failures.joinToString("\n") {
it.getDisplayMessage(applicationContext.resources)
}
notification
.setContentText(if (isRestoreService) getString(R.string.data_not_restored_text) else message)
.setBigText(title, message)
.setSmallIcon(android.R.drawable.stat_notify_error)
result.failures.firstNotNullOfOrNull { error ->
ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, notificationTag)
}?.let { action ->
notification.addAction(action)
}
}
else -> {
notification
.setContentTitle(getString(R.string.restoring_backup))
.setContentText(getString(R.string.data_restored_with_errors))
.setSmallIcon(R.drawable.ic_stat_done)
}
}
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
AppRouter.homeIntent(this@BaseBackupRestoreService),
0,
false,
),
)
if (!isRestoreService && fileUri != null) {
val shareIntent = ShareCompat.IntentBuilder(this@BaseBackupRestoreService)
.setStream(fileUri)
.setType("application/zip")
.setChooserTitle(R.string.share_backup)
.createChooserIntent()
notification.addAction(
appcompatR.drawable.abc_ic_menu_share_mtrl_alpha,
getString(R.string.share),
PendingIntentCompat.getActivity(this@BaseBackupRestoreService, 0, shareIntent, 0, false),
)
}
notificationManager.notify(notificationTag, startId, notification.build())
}
protected fun NotificationCompat.Builder.setBigText(title: String, text: CharSequence) = setStyle(
NotificationCompat.BigTextStyle()
.bigText(text)
.setSummaryText(text)
.setBigContentTitle(title),
)
companion object {
const val CHANNEL_ID = "backup_restore"
fun createNotificationChannel(context: Context) {
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(context.getString(R.string.backup_restore))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
NotificationManagerCompat.from(context).createNotificationChannel(channel)
}
}
}

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.ui.backup
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -14,26 +14,14 @@ import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import java.io.File
@AndroidEntryPoint
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private val viewModel by viewModels<BackupViewModel>()
private val saveFileContract = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip"),
) { uri ->
if (uri != null) {
viewModel.saveBackup(uri)
} else {
dismiss()
}
}
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -47,7 +35,6 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
viewModel.onBackupDone.observeEvent(viewLifecycleOwner, this::onBackupDone)
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
viewModel.onBackupSaved.observeEvent(viewLifecycleOwner) { onBackupSaved() }
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
@@ -77,14 +64,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
}
}
private fun onBackupDone(file: File) {
if (!saveFileContract.tryLaunch(file.name)) {
Toast.makeText(requireContext(), R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
dismiss()
}
}
private fun onBackupSaved() {
private fun onBackupDone(uri: Uri) {
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_SHORT).show()
dismiss()
}

View File

@@ -0,0 +1,131 @@
package org.koitharu.kotatsu.backups.ui.backup
import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.Uri
import android.widget.Toast
import androidx.annotation.CheckResult
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.core.util.progress.Progress
import java.io.FileNotFoundException
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import androidx.appcompat.R as appcompatR
@AndroidEntryPoint
@SuppressLint("InlinedApi")
class BackupService : BaseBackupRestoreService() {
override val notificationTag = TAG
override val isRestoreService = false
@Inject
lateinit var repository: BackupRepository
override suspend fun IntentJobContext.processIntent(intent: Intent) {
val notification = buildNotification(Progress.INDETERMINATE)
setForeground(
FOREGROUND_NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
val destination = intent.getStringExtra(AppRouter.KEY_DATA)?.toUriOrNull() ?: throw FileNotFoundException()
powerManager.withPartialWakeLock(TAG) {
val progress = MutableStateFlow(Progress.INDETERMINATE)
val progressUpdateJob = if (checkNotificationPermission(CHANNEL_ID)) {
launch {
progress.collect {
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification(it))
}
}
} else {
null
}
try {
ZipOutputStream(contentResolver.openOutputStream(destination)).use { output ->
repository.createBackup(output, progress)
}
} catch (e: Throwable) {
try {
DocumentFile.fromSingleUri(applicationContext, destination)?.delete()
} catch (e2: Throwable) {
e.addSuppressed(e2)
}
throw e
}
progressUpdateJob?.cancelAndJoin()
contentResolver.notifyChange(destination, null)
showResultNotification(destination, CompositeResult.success())
withContext(Dispatchers.Main) {
Toast.makeText(this@BackupService, R.string.backup_saved, Toast.LENGTH_SHORT).show()
}
}
}
private fun IntentJobContext.buildNotification(progress: Progress): Notification {
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setContentTitle(getString(R.string.creating_backup))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setOngoing(true)
.setProgress(
progress.total.coerceAtLeast(0),
progress.progress.coerceAtLeast(0),
progress.isIndeterminate,
)
.setContentText(
if (progress.isIndeterminate) {
getString(R.string.processing_)
} else {
getString(R.string.fraction_pattern, progress.progress, progress.total)
},
)
.setSmallIcon(android.R.drawable.stat_sys_upload)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
appcompatR.drawable.abc_ic_clear_material,
applicationContext.getString(android.R.string.cancel),
getCancelIntent(),
).build()
}
companion object {
private const val TAG = "BACKUP"
private const val FOREGROUND_NOTIFICATION_ID = 33
@CheckResult
fun start(context: Context, uri: Uri): Boolean = try {
val intent = Intent(context, BackupService::class.java)
intent.putExtra(AppRouter.KEY_DATA, uri.toString())
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
e.printStackTraceDebug()
false
}
}
}

View File

@@ -0,0 +1,44 @@
package org.koitharu.kotatsu.backups.ui.backup
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.progress.Progress
import java.util.zip.Deflater
import java.util.zip.ZipOutputStream
import javax.inject.Inject
@HiltViewModel
class BackupViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: BackupRepository,
@ApplicationContext context: Context,
) : BaseViewModel() {
val progress = MutableStateFlow(Progress.INDETERMINATE)
val onBackupDone = MutableEventFlow<Uri>()
private val destination = savedStateHandle.require<Uri>(AppRouter.KEY_DATA)
private val contentResolver: ContentResolver = context.contentResolver
init {
launchLoadingJob(Dispatchers.Default) {
ZipOutputStream(checkNotNull(contentResolver.openOutputStream(destination))).use {
it.setLevel(Deflater.BEST_COMPRESSION)
repository.createBackup(it, progress)
}
onBackupDone.call(destination)
}
}
}

View File

@@ -0,0 +1,105 @@
package org.koitharu.kotatsu.backups.ui.periodical
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.domain.BackupUtils
import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import java.util.zip.ZipOutputStream
import javax.inject.Inject
@AndroidEntryPoint
class PeriodicalBackupService : CoroutineIntentService() {
@Inject
lateinit var externalBackupStorage: ExternalBackupStorage
@Inject
lateinit var telegramBackupUploader: TelegramBackupUploader
@Inject
lateinit var repository: BackupRepository
@Inject
lateinit var settings: AppSettings
override suspend fun IntentJobContext.processIntent(intent: Intent) {
if (!settings.isPeriodicalBackupEnabled || settings.periodicalBackupDirectory == null) {
return
}
val lastBackupDate = externalBackupStorage.getLastBackupDate()
if (lastBackupDate != null && lastBackupDate.time + settings.periodicalBackupFrequencyMillis > System.currentTimeMillis()) {
return
}
val output = BackupUtils.createTempFile(applicationContext)
try {
ZipOutputStream(output.outputStream()).use {
repository.createBackup(it, null)
}
externalBackupStorage.put(output)
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
if (settings.isBackupTelegramUploadEnabled && telegramBackupUploader.isAvailable) {
telegramBackupUploader.uploadBackup(output)
}
} finally {
output.delete()
}
}
override fun IntentJobContext.onError(error: Throwable) {
if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) {
return
}
BaseBackupRestoreService.createNotificationChannel(applicationContext)
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setAutoCancel(true)
val title = getString(R.string.periodic_backups)
val message = getString(
R.string.inline_preference_pattern,
getString(R.string.packup_creation_failed),
error.getDisplayMessage(resources),
)
notification
.setContentText(message)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(message)
.setSummaryText(getString(R.string.packup_creation_failed))
.setBigContentTitle(title),
)
ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, TAG)?.let { action ->
notification.addAction(action)
}
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
AppRouter.periodicBackupSettingsIntent(applicationContext),
0,
false,
),
)
NotificationManagerCompat.from(applicationContext).notify(TAG, startId, notification.build())
}
private companion object {
const val CHANNEL_ID = BaseBackupRestoreService.CHANNEL_ID
const val TAG = "periodical_backup"
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.ui.periodical
import android.content.Intent
import android.net.Uri
@@ -9,10 +9,10 @@ import androidx.activity.result.ActivityResultCallback
import androidx.fragment.app.viewModels
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper
@@ -38,6 +38,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_backup_periodic)
findPreference<PreferenceCategory>(AppSettings.KEY_BACKUP_TG)?.isVisible = viewModel.isTelegramAvailable
findPreference<EditTextPreference>(AppSettings.KEY_BACKUP_TG_CHAT)?.summaryProvider =
EditTextFallbackSummaryProvider(R.string.telegram_chat_id_summary)
}
@@ -85,6 +86,11 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
"" -> null
else -> path
}
preference.icon = if (path == null) {
getWarningIcon()
} else {
null
}
}
private fun bindLastBackupInfo(lastBackupDate: Date?) {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.ui.periodical
import android.content.Context
import android.net.Uri
@@ -8,16 +8,14 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.BackupZipOutput.Companion.DIR_BACKUPS
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
import org.koitharu.kotatsu.core.backup.TelegramBackupUploader
import org.koitharu.kotatsu.backups.domain.BackupUtils
import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.resolveFile
import java.io.File
import java.util.Date
import javax.inject.Inject
@@ -29,6 +27,9 @@ class PeriodicalBackupSettingsViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
) : BaseViewModel() {
val isTelegramAvailable
get() = telegramUploader.isAvailable
val lastBackupDate = MutableStateFlow<Date?>(null)
val backupsDirectory = MutableStateFlow<String?>("")
val isTelegramCheckLoading = MutableStateFlow(false)
@@ -60,7 +61,7 @@ class PeriodicalBackupSettingsViewModel @Inject constructor(
backupsDirectory.value = if (dir != null) {
dir.toUserFriendlyString()
} else {
(appContext.getExternalFilesDir(DIR_BACKUPS) ?: File(appContext.filesDir, DIR_BACKUPS)).path
BackupUtils.getAppBackupDir(appContext).path
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.backup
package org.koitharu.kotatsu.backups.ui.periodical
import android.content.Context
import androidx.annotation.CheckResult
@@ -30,6 +30,9 @@ class TelegramBackupUploader @Inject constructor(
private val botToken = context.getString(R.string.tg_backup_bot_token)
val isAvailable: Boolean
get() = botToken.isNotEmpty()
suspend fun uploadBackup(file: File) {
val requestBody = file.asRequestBody("application/zip".toMediaTypeOrNull())
val multipartBody = MultipartBody.Builder()
@@ -90,4 +93,4 @@ class TelegramBackupUploader @Inject constructor(
.host("api.telegram.org")
.addPathSegment("bot$botToken")
.addPathSegment(method)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.ui.restore
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.BaseListAdapter
@@ -8,18 +8,18 @@ import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_CHECKED_CHANGED
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
class BackupEntriesAdapter(
clickListener: OnListItemClickListener<BackupEntryModel>,
) : BaseListAdapter<BackupEntryModel>() {
class BackupSectionsAdapter(
clickListener: OnListItemClickListener<BackupSectionModel>,
) : BaseListAdapter<BackupSectionModel>() {
init {
addDelegate(ListItemType.NAV_ITEM, backupEntryAD(clickListener))
addDelegate(ListItemType.NAV_ITEM, backupSectionAD(clickListener))
}
}
private fun backupEntryAD(
clickListener: OnListItemClickListener<BackupEntryModel>,
) = adapterDelegateViewBinding<BackupEntryModel, BackupEntryModel, ItemCheckableMultipleBinding>(
private fun backupSectionAD(
clickListener: OnListItemClickListener<BackupSectionModel>,
) = adapterDelegateViewBinding<BackupSectionModel, BackupSectionModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {

View File

@@ -0,0 +1,47 @@
package org.koitharu.kotatsu.backups.ui.restore
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.domain.BackupSection
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class BackupSectionModel(
val section: BackupSection,
val isChecked: Boolean,
val isEnabled: Boolean,
) : ListModel {
@get:StringRes
val titleResId: Int
get() = when (section) {
BackupSection.INDEX -> 0 // should not appear here
BackupSection.HISTORY -> R.string.history
BackupSection.CATEGORIES -> R.string.favourites_categories
BackupSection.FAVOURITES -> R.string.favourites
BackupSection.SETTINGS -> R.string.settings
BackupSection.SETTINGS_READER_GRID -> R.string.reader_actions
BackupSection.BOOKMARKS -> R.string.bookmarks
BackupSection.SOURCES -> R.string.remote_sources
BackupSection.SCROBBLING -> R.string.tracking
BackupSection.STATS -> R.string.statistics
BackupSection.SAVED_FILTERS -> R.string.saved_filters
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is BackupSectionModel && other.section == section
}
override fun getChangePayload(previousState: ListModel): Any? {
if (previousState !is BackupSectionModel) {
return null
}
return if (previousState.isEnabled != isEnabled) {
ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
} else if (previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.backup
package org.koitharu.kotatsu.backups.ui.restore
import android.os.Bundle
import android.view.LayoutInflater
@@ -25,7 +25,7 @@ import java.text.SimpleDateFormat
import java.util.Date
@AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupSectionModel>,
View.OnClickListener {
private val viewModel: RestoreViewModel by viewModels()
@@ -37,7 +37,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
override fun onViewBindingCreated(binding: DialogRestoreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = BackupEntriesAdapter(this)
val adapter = BackupSectionsAdapter(this)
binding.recyclerView.adapter = adapter
binding.buttonCancel.setOnClickListener(this)
binding.buttonRestore.setOnClickListener(this)
@@ -72,11 +72,11 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
}
}
override fun onItemClick(item: BackupEntryModel, view: View) {
override fun onItemClick(item: BackupSectionModel, view: View) {
viewModel.onItemClick(item)
}
private fun onLoadingChanged(value: Triple<Boolean, List<BackupEntryModel>, Date?>) {
private fun onLoadingChanged(value: Triple<Boolean, List<BackupSectionModel>, Date?>) {
val (isLoading, entries, backupDate) = value
val hasEntries = entries.isNotEmpty()
with(requireViewBinding()) {
@@ -96,7 +96,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
return RestoreService.start(
context ?: return false,
viewModel.uri ?: return false,
viewModel.getCheckedEntries(),
viewModel.getCheckedSections(),
)
}

View File

@@ -0,0 +1,118 @@
package org.koitharu.kotatsu.backups.ui.restore
import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.Uri
import androidx.annotation.CheckResult
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.domain.BackupSection
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.core.util.progress.Progress
import java.io.FileNotFoundException
import java.util.zip.ZipInputStream
import javax.inject.Inject
import androidx.appcompat.R as appcompatR
@AndroidEntryPoint
@SuppressLint("InlinedApi")
class RestoreService : BaseBackupRestoreService() {
override val notificationTag = TAG
override val isRestoreService = true
@Inject
lateinit var repository: BackupRepository
override suspend fun IntentJobContext.processIntent(intent: Intent) {
val notification = buildNotification(Progress.INDETERMINATE)
setForeground(
FOREGROUND_NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
val source = intent.getStringExtra(AppRouter.KEY_DATA)?.toUriOrNull() ?: throw FileNotFoundException()
val sections =
requireNotNull(intent.getSerializableExtraCompat<Array<BackupSection>>(AppRouter.KEY_ENTRIES)?.toSet())
powerManager.withPartialWakeLock(TAG) {
val progress = MutableStateFlow(Progress.INDETERMINATE)
val progressUpdateJob = if (checkNotificationPermission(CHANNEL_ID)) {
launch {
progress.collect {
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification(it))
}
}
} else {
null
}
val result = ZipInputStream(contentResolver.openInputStream(source)).use { input ->
repository.restoreBackup(input, sections, progress)
}
progressUpdateJob?.cancelAndJoin()
showResultNotification(source, result)
}
}
private fun IntentJobContext.buildNotification(progress: Progress): Notification {
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setContentTitle(getString(R.string.restoring_backup))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setOngoing(true)
.setProgress(
progress.total.coerceAtLeast(0),
progress.progress.coerceAtLeast(0),
progress.isIndeterminate,
)
.setContentText(
if (progress.isIndeterminate) {
getString(R.string.processing_)
} else {
getString(R.string.fraction_pattern, progress.progress, progress.total)
},
)
.setSmallIcon(android.R.drawable.stat_sys_upload)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
appcompatR.drawable.abc_ic_clear_material,
applicationContext.getString(android.R.string.cancel),
getCancelIntent(),
).build()
}
companion object {
private const val TAG = "RESTORE"
private const val FOREGROUND_NOTIFICATION_ID = 39
@CheckResult
fun start(context: Context, uri: Uri, sections: Set<BackupSection>): Boolean = try {
val intent = Intent(context, RestoreService::class.java)
intent.putExtra(AppRouter.KEY_DATA, uri.toString())
intent.putExtra(AppRouter.KEY_ENTRIES, sections.toTypedArray())
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
e.printStackTraceDebug()
false
}
}
}

View File

@@ -0,0 +1,112 @@
package org.koitharu.kotatsu.backups.ui.restore
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runInterruptible
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.koitharu.kotatsu.backups.data.model.BackupIndex
import org.koitharu.kotatsu.backups.domain.BackupSection
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.Date
import java.util.EnumMap
import java.util.EnumSet
import java.util.zip.ZipInputStream
import javax.inject.Inject
@HiltViewModel
class RestoreViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@ApplicationContext context: Context,
) : BaseViewModel() {
val uri = savedStateHandle.get<String>(AppRouter.KEY_FILE)?.toUriOrNull()
private val contentResolver = context.contentResolver
val availableEntries = MutableStateFlow<List<BackupSectionModel>>(emptyList())
val backupDate = MutableStateFlow<Date?>(null)
init {
launchLoadingJob(Dispatchers.Default) {
loadBackupInfo()
}
}
private suspend fun loadBackupInfo() {
val sections = runInterruptible(Dispatchers.IO) {
if (uri == null) throw FileNotFoundException()
ZipInputStream(contentResolver.openInputStream(uri)).use { stream ->
val result = EnumSet.noneOf(BackupSection::class.java)
var entry = stream.nextEntry
while (entry != null) {
val s = BackupSection.of(entry)
if (s != null) {
result.add(s)
if (s == BackupSection.INDEX) {
backupDate.value = stream.readDate()
}
}
stream.closeEntry()
entry = stream.nextEntry
}
result
}
}
availableEntries.value = BackupSection.entries.mapNotNull { entry ->
if (entry == BackupSection.INDEX || entry !in sections) {
return@mapNotNull null
}
BackupSectionModel(
section = entry,
isChecked = true,
isEnabled = true,
)
}
}
fun onItemClick(item: BackupSectionModel) {
val map = availableEntries.value.associateByTo(EnumMap(BackupSection::class.java)) { it.section }
map[item.section] = item.copy(isChecked = !item.isChecked)
map.validate()
availableEntries.value = map.values.sortedBy { it.section.ordinal }
}
fun getCheckedSections(): Set<BackupSection> = availableEntries.value
.mapNotNullTo(EnumSet.noneOf(BackupSection::class.java)) {
if (it.isChecked) it.section else null
}
/**
* Check for inconsistent user selection
* Favorites cannot be restored without categories
*/
private fun MutableMap<BackupSection, BackupSectionModel>.validate() {
val favorites = this[BackupSection.FAVOURITES] ?: return
val categories = this[BackupSection.CATEGORIES]
if (categories?.isChecked == true) {
if (!favorites.isEnabled) {
this[BackupSection.FAVOURITES] = favorites.copy(isEnabled = true)
}
} else {
if (favorites.isEnabled) {
this[BackupSection.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
}
}
}
private fun InputStream.readDate(): Date? = runCatching {
val index = Json.decodeFromStream<List<BackupIndex>>(this)
Date(index.single().createdAt)
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrNull()
}

View File

@@ -6,7 +6,10 @@ import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao
@@ -47,4 +50,17 @@ abstract class BookmarksDao {
@Upsert
abstract suspend fun upsert(bookmarks: Collection<BookmarkEntity>)
fun dump(): Flow<Pair<MangaWithTags, List<BookmarkEntity>>> = flow {
val window = 4
var offset = 0
while (currentCoroutineContext().isActive) {
val list = findAll(offset, window)
if (list.isEmpty()) {
break
}
offset += window
list.forEach { emit(it.key to it.value) }
}
}
}

View File

@@ -1,7 +1,8 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.isImage
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.local.data.hasImageExtension
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.time.Instant
@@ -17,9 +18,6 @@ data class Bookmark(
val percent: Float,
) : ListModel {
val imageLoadData: Any
get() = if (isImageUrlDirect()) imageUrl else toMangaPage()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Bookmark &&
manga.id == other.manga.id &&
@@ -30,11 +28,9 @@ data class Bookmark(
fun toMangaPage() = MangaPage(
id = pageId,
url = imageUrl,
preview = null,
preview = imageUrl.takeIf {
MimeTypes.getMimeTypeFromUrl(it)?.isImage == true
},
source = manga.source,
)
private fun isImageUrlDirect(): Boolean {
return hasImageExtension(imageUrl)
}
}

View File

@@ -12,7 +12,6 @@ import androidx.appcompat.view.ActionMode
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@@ -40,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
import javax.inject.Inject
@AndroidEntryPoint
@@ -50,16 +50,22 @@ class AllBookmarksFragment :
ListSelectionController.Callback,
FastScroller.FastScrollListener, ListHeaderClickListener {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
@Inject
lateinit var pageSaveHelperFactory: PageSaveHelper.Factory
private lateinit var pageSaveHelper: PageSaveHelper
private val viewModel by viewModels<AllBookmarksViewModel>()
private var bookmarksAdapter: BookmarksAdapter? = null
private var selectionController: ListSelectionController? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageSaveHelper = pageSaveHelperFactory.create(this)
}
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -79,8 +85,6 @@ class AllBookmarksFragment :
callback = this,
)
bookmarksAdapter = BookmarksAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
clickListener = this,
headerClickListener = this,
)
@@ -129,7 +133,7 @@ class AllBookmarksFragment :
if (selectionController?.onItemClick(item.pageId) != true) {
val intent = ReaderIntent.Builder(view.context)
.bookmark(item)
.incognito(true)
.incognito()
.build()
router.openReader(intent)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
@@ -185,6 +189,12 @@ class AllBookmarksFragment :
true
}
R.id.action_save -> {
viewModel.savePages(pageSaveHelper, selectionController?.snapshot() ?: return false)
mode?.finish()
true
}
else -> false
}
}

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
import javax.inject.Inject
@HiltViewModel
@@ -56,6 +57,23 @@ class AllBookmarksViewModel @Inject constructor(
}
}
fun savePages(pageSaveHelper: PageSaveHelper, ids: Set<Long>) {
launchLoadingJob(Dispatchers.Default) {
val tasks = content.value.mapNotNull {
if (it !is Bookmark || it.pageId !in ids) return@mapNotNull null
PageSaveHelper.Task(
manga = it.manga,
chapterId = it.chapterId,
pageNumber = it.page + 1,
page = it.toMangaPage(),
)
}
val dest = pageSaveHelper.save(tasks)
val msg = if (dest.size == 1) R.string.page_saved else R.string.pages_saved
onActionDone.call(ReversibleAction(msg, null))
}
}
private fun mapList(data: Map<Manga, List<Bookmark>>): List<ListModel> {
val result = ArrayList<ListModel>(data.values.sumOf { it.size + 1 })
for ((manga, bookmarks) in data) {

View File

@@ -1,24 +1,13 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
fun bookmarkLargeAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) = adapterDelegateViewBinding<Bookmark, ListModel, ItemBookmarkLargeBinding>(
{ inflater, parent -> ItemBookmarkLargeBinding.inflate(inflater, parent, false) },
@@ -26,14 +15,7 @@ fun bookmarkLargeAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
defaultPlaceholders(context)
allowRgb565(true)
bookmarkExtra(item)
decodeRegion(item.scroll)
enqueueWith(coil)
}
binding.imageViewThumb.setImageAsync(item)
binding.progressView.setProgress(item.percent, false)
}
}

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -17,19 +15,17 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.browser
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import javax.inject.Inject
@AndroidEntryPoint
class AdListUpdateService : CoroutineIntentService() {
@Inject
lateinit var updater: AdBlock.Updater
override suspend fun IntentJobContext.processIntent(intent: Intent) {
updater.updateList()
}
override fun IntentJobContext.onError(error: Throwable) = Unit
}

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity
@@ -29,6 +30,9 @@ abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), Bro
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var adBlock: AdBlock
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,13 +1,17 @@
package org.koitharu.kotatsu.browser
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContract
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
@@ -20,7 +24,7 @@ class BrowserActivity : BaseBrowserActivity() {
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webViewClient = BrowserClient(this, adBlock)
lifecycleScope.launch {
try {
proxyProvider.applyWebViewConfig()
@@ -65,4 +69,23 @@ class BrowserActivity : BaseBrowserActivity() {
else -> super.onOptionsItemSelected(item)
}
class Contract : ActivityResultContract<InteractiveActionRequiredException, Unit>() {
override fun createIntent(
context: Context,
input: InteractiveActionRequiredException
): Intent = AppRouter.browserIntent(
context = context,
url = input.url,
source = input.source,
title = null,
)
override fun parseResult(resultCode: Int, intent: Intent?): Unit = Unit
}
companion object {
const val TAG = "BrowserActivity"
}
}

View File

@@ -1,12 +1,27 @@
package org.koitharu.kotatsu.browser
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.webkit.WebViewClientCompat
import android.webkit.WebViewClient
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
import java.io.ByteArrayInputStream
open class BrowserClient(
private val callback: BrowserCallback
) : WebViewClientCompat() {
private val callback: BrowserCallback,
private val adBlock: AdBlock?,
) : WebViewClient() {
/**
* https://stackoverflow.com/questions/57414530/illegalstateexception-reasonphrase-cant-be-empty-with-android-webview
*/
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
@@ -27,4 +42,39 @@ open class BrowserClient(
super.doUpdateVisitedHistory(view, url, isReload)
callback.onHistoryChanged()
}
@WorkerThread
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(
view: WebView?,
url: String?
): WebResourceResponse? = if (url.isNullOrEmpty() || adBlock?.shouldLoadUrl(url, view?.getUrlSafe()) ?: true) {
super.shouldInterceptRequest(view, url)
} else {
emptyResponse()
}
@WorkerThread
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? =
if (request == null || adBlock?.shouldLoadUrl(request.url.toString(), view?.getUrlSafe()) ?: true) {
super.shouldInterceptRequest(view, request)
} else {
emptyResponse()
}
private fun emptyResponse(): WebResourceResponse =
WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream(byteArrayOf()))
@SuppressLint("WrongThread")
@AnyThread
private fun WebView.getUrlSafe(): String? = if (Looper.myLooper() == Looper.getMainLooper()) {
url
} else {
runBlocking(Dispatchers.Main.immediate) {
url
}
}
}

View File

@@ -1,111 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import coil3.EventListener
import coil3.Extras
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.parsers.model.MangaSource
class CaptchaNotifier(
private val context: Context,
) : EventListener() {
fun notify(exception: CloudFlareProtectedException) {
if (!context.checkNotificationPermission(CHANNEL_ID)) {
return
}
if (exception.source != null && SourceSettings(context, exception.source).isCaptchaNotificationsDisabled) {
return
}
val manager = NotificationManagerCompat.from(context)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.captcha_required))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
manager.createNotificationChannel(channel)
val intent = AppRouter.cloudFlareResolveIntent(context, exception)
.setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setDefaults(0)
.setSmallIcon(R.drawable.ic_bot)
.setGroup(GROUP_CAPTCHA)
.setAutoCancel(true)
.setVisibility(
if (exception.source?.isNsfw() == true) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
)
.setContentText(
context.getString(
R.string.captcha_required_summary,
exception.source?.getTitle(context) ?: context.getString(R.string.app_name),
),
)
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val actionIntent = PendingIntentCompat.getActivity(
context, SETTINGS_ACTION_CODE,
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID),
0, false,
)
notification.addAction(
R.drawable.ic_settings,
context.getString(R.string.notifications_settings),
actionIntent,
)
}
manager.notify(TAG, exception.source.hashCode(), notification.build())
}
fun dismiss(source: MangaSource) {
NotificationManagerCompat.from(context).cancel(TAG, source.hashCode())
}
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
val e = result.throwable
if (e is CloudFlareProtectedException && request.extras[ignoreCaptchaKey] != true) {
notify(e)
}
}
companion object {
fun ImageRequest.Builder.ignoreCaptchaErrors() = apply {
extras[ignoreCaptchaKey] = true
}
val ignoreCaptchaKey = Extras.Key(false)
private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
private const val SETTINGS_ACTION_CODE = 3
}
}

View File

@@ -19,14 +19,17 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BaseBrowserActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
@AndroidEntryPoint
@@ -37,6 +40,9 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
@Inject
lateinit var cookieJar: MutableCookieJar
@Inject
lateinit var captchaHandler: CaptchaHandler
private lateinit var cfClient: CloudFlareClient
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
@@ -46,7 +52,7 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
finishAfterTransition()
return
}
cfClient = CloudFlareClient(cookieJar, this, url)
cfClient = CloudFlareClient(cookieJar, this, adBlock, url)
viewBinding.webView.webViewClient = cfClient
lifecycleScope.launch {
try {
@@ -98,11 +104,17 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
override fun onCheckPassed() {
pendingResult = RESULT_OK
val source = intent?.getStringExtra(AppRouter.KEY_SOURCE)
if (source != null) {
CaptchaNotifier(this).dismiss(MangaSource(source))
lifecycleScope.launch {
val source = intent?.getStringExtra(AppRouter.KEY_SOURCE)
if (source != null) {
runCatchingCancellable {
captchaHandler.discard(MangaSource(source))
}.onFailure {
it.printStackTraceDebug()
}
}
finishAfterTransition()
}
finishAfterTransition()
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {

View File

@@ -4,6 +4,7 @@ import android.graphics.Bitmap
import android.webkit.WebView
import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
private const val LOOP_COUNTER = 3
@@ -11,8 +12,9 @@ private const val LOOP_COUNTER = 3
class CloudFlareClient(
private val cookieJar: MutableCookieJar,
private val callback: CloudFlareCallback,
adBlock: AdBlock,
private val targetUrl: String,
) : BrowserClient(callback) {
) : BrowserClient(callback, adBlock) {
private val oldClearance = getClearance()
private var counter = 0

View File

@@ -31,8 +31,9 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.backups.domain.BackupObserver
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler
import org.koitharu.kotatsu.core.image.AvifImageDecoder
import org.koitharu.kotatsu.core.image.CbzFetcher
import org.koitharu.kotatsu.core.image.MangaSourceHeaderInterceptor
@@ -41,25 +42,27 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.AcraScreenLogger
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.FaviconCache
import org.koitharu.kotatsu.local.data.LocalStorageCache
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PageCache
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.widget.WidgetUpdater
import javax.inject.Provider
@@ -101,11 +104,12 @@ interface AppModule {
fun provideCoil(
@LocalizedAppContext context: Context,
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
mangaRepositoryFactory: MangaRepository.Factory,
faviconFetcherFactory: FaviconFetcher.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
coverRestoreInterceptor: CoverRestoreInterceptor,
networkStateProvider: Provider<NetworkState>,
captchaHandler: CaptchaHandler,
): ImageLoader {
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -121,7 +125,7 @@ interface AppModule {
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.eventListener(CaptchaNotifier(context))
.eventListener(captchaHandler)
.components {
add(
OkHttpNetworkFetcherFactory(
@@ -137,7 +141,7 @@ interface AppModule {
add(SvgDecoder.Factory())
add(CbzFetcher.Factory())
add(AvifImageDecoder.Factory())
add(FaviconFetcher.Factory(mangaRepositoryFactory))
add(faviconFetcherFactory)
add(MangaPageKeyer())
add(pageFetcherFactory)
add(imageProxyInterceptor)
@@ -194,5 +198,29 @@ interface AppModule {
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager = WorkManager.getInstance(context)
@Provides
@Singleton
@PageCache
fun providePageCache(
@ApplicationContext context: Context,
) = LocalStorageCache(
context = context,
dir = CacheDir.PAGES,
defaultSize = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
minSize = FileSize.MEGABYTES.convert(20, FileSize.BYTES),
)
@Provides
@Singleton
@FaviconCache
fun provideFaviconCache(
@ApplicationContext context: Context,
) = LocalStorageCache(
context = context,
dir = CacheDir.FAVICONS,
defaultSize = FileSize.MEGABYTES.convert(8, FileSize.BYTES),
minSize = FileSize.MEGABYTES.convert(2, FileSize.BYTES),
)
}
}

View File

@@ -8,11 +8,11 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import okhttp3.internal.platform.PlatformRegistry
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.dialog
@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.os.RomCompat
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
@@ -62,9 +61,6 @@ open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var workScheduleManager: WorkScheduleManager
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
@Inject
lateinit var localMangaIndexProvider: Provider<LocalMangaIndex>
@@ -79,6 +75,7 @@ open class BaseApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
PlatformRegistry.applicationContext = this // TODO replace with OkHttp.initialize
if (ACRA.isACRASenderServiceProcess()) {
return
}
@@ -97,7 +94,6 @@ open class BaseApp : Application(), Configuration.Provider {
localStorageChanges.collect(localMangaIndexProvider.get())
}
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()
}
override fun attachBaseContext(base: Context) {

View File

@@ -4,10 +4,12 @@ import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.BadParcelableException
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -17,22 +19,62 @@ class ErrorReporterReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 0)
if (notificationId != 0 && context != null) {
val notificationTag = intent.getStringExtra(EXTRA_NOTIFICATION_TAG)
NotificationManagerCompat.from(context).cancel(notificationTag, notificationId)
}
e.report()
}
companion object {
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
private const val EXTRA_NOTIFICATION_ID = "notify.id"
private const val EXTRA_NOTIFICATION_TAG = "notify.tag"
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try {
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = getPendingIntentInternal(
context = context,
e = e,
notificationId = 0,
notificationTag = null,
)
fun getNotificationAction(
context: Context,
e: Throwable,
notificationId: Int,
notificationTag: String?,
): NotificationCompat.Action? {
val intent = getPendingIntentInternal(
context = context,
e = e,
notificationId = notificationId,
notificationTag = notificationTag,
) ?: return null
return NotificationCompat.Action(
R.drawable.ic_alert_outline,
context.getString(R.string.report),
intent,
)
}
private fun getPendingIntentInternal(
context: Context,
e: Throwable,
notificationId: Int,
notificationTag: String?,
): PendingIntent? = runCatching {
val intent = Intent(context, ErrorReporterReceiver::class.java)
intent.setAction(ACTION_REPORT)
intent.setData(Uri.parse("err://${e.hashCode()}"))
intent.setData("err://${e.hashCode()}".toUri())
intent.putExtra(AppRouter.KEY_ERROR, e)
intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId)
intent.putExtra(EXTRA_NOTIFICATION_TAG, notificationTag)
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
} catch (e: BadParcelableException) {
}.onFailure { e ->
// probably cannot write exception as serializable
e.printStackTraceDebug()
null
}
}.getOrNull()
}
}

View File

@@ -1,23 +0,0 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
class BackupEntry(
val name: Name,
val data: JSONArray
) {
enum class Name(
val key: String,
) {
INDEX("index"),
HISTORY("history"),
CATEGORIES("categories"),
FAVOURITES("favourites"),
SETTINGS("settings"),
SETTINGS_READER_GRID("reader_grid"),
BOOKMARKS("bookmarks"),
SOURCES("sources"),
}
}

View File

@@ -1,259 +0,0 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import kotlinx.coroutines.flow.FlowCollector
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.parsers.util.json.asTypedList
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.util.Date
import javax.inject.Inject
private const val PAGE_SIZE = 10
class BackupRepository @Inject constructor(
private val db: MangaDatabase,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
) {
suspend fun dumpHistory(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
while (true) {
val history = db.getHistoryDao().findAll(offset = offset, limit = PAGE_SIZE)
if (history.isEmpty()) {
break
}
offset += history.size
for (item in history) {
val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags)
val json = JsonSerializer(item.history).toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
val categories = db.getFavouriteCategoriesDao().findAll()
for (item in categories) {
entry.data.put(JsonSerializer(item).toJson())
}
return entry
}
suspend fun dumpFavourites(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
while (true) {
val favourites = db.getFavouritesDao().findAllRaw(offset = offset, limit = PAGE_SIZE)
if (favourites.isEmpty()) {
break
}
offset += favourites.size
for (item in favourites) {
val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags)
val json = JsonSerializer(item.favourite).toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun dumpBookmarks(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
while (true) {
val bookmarks = db.getBookmarksDao().findAll(offset = offset, limit = PAGE_SIZE)
if (bookmarks.isEmpty()) {
break
}
offset += bookmarks.size
for ((m, b) in bookmarks) {
val json = JSONObject()
val manga = JsonSerializer(m.manga).toJson()
json.put("manga", manga)
val tags = JSONArray()
m.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
json.put("tags", tags)
val bookmarks = JSONArray()
b.forEach { bookmarks.put(JsonSerializer(it).toJson()) }
json.put("bookmarks", bookmarks)
entry.data.put(json)
}
}
return entry
}
fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
val settingsDump = settings.getAllValues().toMutableMap()
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_LOGIN)
settingsDump.remove(AppSettings.KEY_INCOGNITO_MODE)
val json = JsonSerializer(settingsDump).toJson()
entry.data.put(json)
return entry
}
fun dumpReaderGridSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SETTINGS_READER_GRID, JSONArray())
val settingsDump = tapGridSettings.getAllValues()
val json = JsonSerializer(settingsDump).toJson()
entry.data.put(json)
return entry
}
suspend fun dumpSources(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
val all = db.getSourcesDao().findAll()
for (source in all) {
val json = JsonSerializer(source).toJson()
entry.data.put(json)
}
return entry
}
fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE)
json.put("created_at", System.currentTimeMillis())
entry.data.put(json)
return entry
}
fun getBackupDate(entry: BackupEntry?): Date? {
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
return if (timestamp == 0L) null else Date(timestamp)
}
suspend fun restoreHistory(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
val result = CompositeResult()
val list = entry.data.asTypedList<JSONObject>()
outProgress?.emit(Progress(progress = 0, total = list.size))
for ((index, item) in list.withIndex()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val history = JsonDeserializer(item).toHistoryEntity()
result += runCatchingCancellable {
db.withTransaction {
db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga, tags)
db.getHistoryDao().upsert(history)
}
}
outProgress?.emit(Progress(progress = index, total = list.size))
}
return result
}
suspend fun restoreCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatchingCancellable {
db.getFavouriteCategoriesDao().upsert(category)
}
}
return result
}
suspend fun restoreFavourites(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
val result = CompositeResult()
val list = entry.data.asTypedList<JSONObject>()
outProgress?.emit(Progress(progress = 0, total = list.size))
for ((index, item) in list.withIndex()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatchingCancellable {
db.withTransaction {
db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga, tags)
db.getFavouritesDao().upsert(favourite)
}
}
outProgress?.emit(Progress(progress = index, total = list.size))
}
return result
}
suspend fun restoreBookmarks(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = item.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val bookmarks = item.getJSONArray("bookmarks").mapJSON {
JsonDeserializer(it).toBookmarkEntity()
}
result += runCatchingCancellable {
db.withTransaction {
db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga, tags)
db.getBookmarksDao().upsert(bookmarks)
}
}
}
return result
}
suspend fun restoreSources(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
val source = JsonDeserializer(item).toMangaSourceEntity()
result += runCatchingCancellable {
db.getSourcesDao().upsert(source)
}
}
return result
}
fun restoreSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
result += runCatchingCancellable {
settings.upsertAll(JsonDeserializer(item).toMap())
}
}
return result
}
fun restoreReaderGridSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
result += runCatchingCancellable {
tapGridSettings.upsertAll(JsonDeserializer(item).toMap())
}
}
return result
}
}

View File

@@ -1,61 +0,0 @@
package org.koitharu.kotatsu.core.backup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.internal.closeQuietly
import okio.Closeable
import org.json.JSONArray
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import java.io.File
import java.util.EnumSet
import java.util.zip.ZipException
import java.util.zip.ZipFile
class BackupZipInput private constructor(val file: File) : Closeable {
private val zipFile = ZipFile(file)
suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
val json = zipFile.getInputStream(entry).use {
JSONArray(it.bufferedReader().readText())
}
BackupEntry(name, json)
}
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
BackupEntry.Name.entries.find { it.key == ze.name }
}
}
override fun close() {
zipFile.close()
}
fun closeAndDelete() {
closeQuietly()
file.delete()
}
companion object {
fun from(file: File): BackupZipInput {
var res: BackupZipInput? = null
return try {
res = BackupZipInput(file)
if (res.zipFile.getEntry("index") == null) {
throw BadBackupFormatException(null)
}
res
} catch (exception: Throwable) {
res?.closeQuietly()
throw if (exception is ZipException) {
BadBackupFormatException(exception)
} else {
exception
}
}
}
}
}

View File

@@ -1,60 +0,0 @@
package org.koitharu.kotatsu.core.backup
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.Closeable
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.zip.ZipOutput
import java.io.File
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.zip.Deflater
class BackupZipOutput(val file: File) : Closeable {
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
output.put(entry.name.key, entry.data.toString(2))
}
suspend fun finish() = runInterruptible(Dispatchers.IO) {
output.finish()
}
override fun close() {
output.close()
}
companion object {
const val DIR_BACKUPS = "backups"
private val dateTimeFormat = SimpleDateFormat("yyyyMMdd-HHmm")
fun generateFileName(context: Context) = buildString {
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
append('_')
append(dateTimeFormat.format(Date()))
append(".bk.zip")
}
fun parseBackupDateTime(fileName: String): Date? = try {
dateTimeFormat.parse(fileName.substringAfterLast('_').substringBefore('.'))
} catch (e: ParseException) {
e.printStackTraceDebug()
null
}
suspend fun createTemp(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}
dir.mkdirs()
BackupZipOutput(File(dir, generateFileName(context)))
}
}
}

View File

@@ -1,46 +0,0 @@
package org.koitharu.kotatsu.core.backup
class CompositeResult {
private var successCount: Int = 0
private val errors = ArrayList<Throwable?>()
val size: Int
get() = successCount + errors.size
val failures: List<Throwable>
get() = errors.filterNotNull()
val isEmpty: Boolean
get() = errors.isEmpty() && successCount == 0
val isAllSuccess: Boolean
get() = errors.none { it != null }
val isAllFailed: Boolean
get() = successCount == 0 && errors.isNotEmpty()
operator fun plusAssign(result: Result<*>) {
when {
result.isSuccess -> successCount++
result.isFailure -> errors.add(result.exceptionOrNull())
}
}
operator fun plusAssign(error: Throwable) {
errors.add(error)
}
operator fun plusAssign(other: CompositeResult) {
this.successCount += other.successCount
this.errors += other.errors
}
operator fun plus(other: CompositeResult): CompositeResult {
val result = CompositeResult()
result.successCount = this.successCount + other.successCount
result.errors.addAll(this.errors)
result.errors.addAll(other.errors)
return result
}
}

View File

@@ -1,106 +0,0 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
class JsonDeserializer(private val json: JSONObject) {
fun toFavouriteEntity() = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
sortKey = json.getIntOrDefault("sort_key", 0),
createdAt = json.getLong("created_at"),
deletedAt = 0L,
)
fun toMangaEntity() = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitles = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false),
contentRating = json.getStringOrNull("content_rating"),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
authors = json.getStringOrNull("author"),
source = json.getString("source"),
)
fun toTagEntity() = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source"),
)
fun toHistoryEntity() = HistoryEntity(
mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat(),
percent = json.getFloatOrDefault("percent", -1f),
chaptersCount = json.getIntOrDefault("chapters", -1),
deletedAt = 0L,
)
fun toFavouriteCategoryEntity() = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
track = json.getBooleanOrDefault("track", true),
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
deletedAt = 0L,
)
fun toBookmarkEntity() = BookmarkEntity(
mangaId = json.getLong("manga_id"),
pageId = json.getLong("page_id"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getInt("scroll"),
imageUrl = json.getString("image_url"),
createdAt = json.getLong("created_at"),
percent = json.getDouble("percent").toFloat(),
)
fun toMangaSourceEntity() = MangaSourceEntity(
source = json.getString("source"),
isEnabled = json.getBoolean("enabled"),
sortKey = json.getInt("sort_key"),
addedIn = json.getIntOrDefault("added_in", 0),
lastUsedAt = json.getLongOrDefault("used_at", 0L),
isPinned = json.getBooleanOrDefault("pinned", false),
)
fun toMap(): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
val keys = json.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = json.get(key)
map[key] = value
}
return map
}
}

View File

@@ -1,104 +0,0 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
class JsonSerializer private constructor(private val json: JSONObject) {
constructor(e: FavouriteEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("category_id", e.categoryId)
put("sort_key", e.sortKey)
put("created_at", e.createdAt)
},
)
constructor(e: FavouriteCategoryEntity) : this(
JSONObject().apply {
put("category_id", e.categoryId)
put("created_at", e.createdAt)
put("sort_key", e.sortKey)
put("title", e.title)
put("order", e.order)
put("track", e.track)
put("show_in_lib", e.isVisibleInLibrary)
},
)
constructor(e: HistoryEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("created_at", e.createdAt)
put("updated_at", e.updatedAt)
put("chapter_id", e.chapterId)
put("page", e.page)
put("scroll", e.scroll)
put("percent", e.percent)
put("chapters", e.chaptersCount)
},
)
constructor(e: TagEntity) : this(
JSONObject().apply {
put("id", e.id)
put("title", e.title)
put("key", e.key)
put("source", e.source)
},
)
constructor(e: MangaEntity) : this(
JSONObject().apply {
put("id", e.id)
put("title", e.title)
put("alt_title", e.altTitles)
put("url", e.url)
put("public_url", e.publicUrl)
put("rating", e.rating)
put("nsfw", e.isNsfw)
put("content_rating", e.contentRating)
put("cover_url", e.coverUrl)
put("large_cover_url", e.largeCoverUrl)
put("state", e.state)
put("author", e.authors)
put("source", e.source)
},
)
constructor(e: BookmarkEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("page_id", e.pageId)
put("chapter_id", e.chapterId)
put("page", e.page)
put("scroll", e.scroll)
put("image_url", e.imageUrl)
put("created_at", e.createdAt)
put("percent", e.percent)
},
)
constructor(e: MangaSourceEntity) : this(
JSONObject().apply {
put("source", e.source)
put("enabled", e.isEnabled)
put("sort_key", e.sortKey)
put("added_in", e.addedIn)
put("used_at", e.lastUsedAt)
put("pinned", e.isPinned)
},
)
constructor(m: Map<String, *>) : this(
JSONObject(m),
)
fun toJson(): JSONObject = json
}

View File

@@ -41,6 +41,8 @@ import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration23To24
import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To25
import org.koitharu.kotatsu.core.db.migrations.Migration25To26
import org.koitharu.kotatsu.core.db.migrations.Migration26To27
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -68,7 +70,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 25
const val DATABASE_VERSION = 27
@Database(
entities = [
@@ -138,6 +140,8 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration23To24(),
Migration24To23(),
Migration24To25(),
Migration25To26(),
Migration26To27(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -8,3 +8,4 @@ const val TABLE_HISTORY = "history"
const val TABLE_MANGA_TAGS = "manga_tags"
const val TABLE_SOURCES = "sources"
const val TABLE_CHAPTERS = "chapters"
const val TABLE_PREFERENCES = "preferences"

View File

@@ -34,6 +34,9 @@ abstract class MangaDao {
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
@Query("SELECT author FROM manga WHERE manga.source = :source AND author IS NOT NULL AND author != '' GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
abstract suspend fun findAuthorsBySource(source: String, limit: Int): List<String>
@Transaction
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>

View File

@@ -9,10 +9,15 @@ import androidx.room.Transaction
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper.PROTECTION_CAPTCHA
@Dao
abstract class MangaSourcesDao {
@@ -50,6 +55,9 @@ abstract class MangaSourcesDao {
@Query("UPDATE sources SET pinned = :isPinned WHERE source = :source")
abstract suspend fun setPinned(source: String, isPinned: Boolean)
@Query("UPDATE sources SET cf_state = :state WHERE source = :source")
abstract suspend fun setCfState(source: String, state: Int)
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Transaction
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
@@ -60,6 +68,9 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE pinned = 1")
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE cf_state = $PROTECTION_CAPTCHA")
abstract suspend fun findAllCaptchaRequired(): List<MangaSourceEntity>
fun observeAll(enabledOnly: Boolean, order: SourcesSortOrder): Flow<List<MangaSourceEntity>> =
observeImpl(getQuery(enabledOnly, order))
@@ -76,11 +87,25 @@ abstract class MangaSourcesDao {
addedIn = BuildConfig.VERSION_CODE,
lastUsedAt = 0,
isPinned = false,
cfState = CloudFlareHelper.PROTECTION_NOT_DETECTED,
)
upsert(entity)
}
}
fun dumpEnabled(): Flow<MangaSourceEntity> = flow {
val window = 10
var offset = 0
while (currentCoroutineContext().isActive) {
val list = findAllEnabled(offset, window)
if (list.isEmpty()) {
break
}
offset += window
list.forEach { emit(it) }
}
}
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int
@@ -90,6 +115,9 @@ abstract class MangaSourcesDao {
@RawQuery
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY source LIMIT :limit OFFSET :offset")
protected abstract suspend fun findAllEnabled(offset: Int, limit: Int): List<MangaSourceEntity>
private fun getQuery(enabledOnly: Boolean, order: SourcesSortOrder) = SimpleSQLiteQuery(
buildString {
append("SELECT * FROM sources ")

View File

@@ -15,6 +15,9 @@ abstract class PreferencesDao {
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
@Query("SELECT * FROM preferences WHERE title_override IS NOT NULL OR cover_override IS NOT NULL OR content_rating_override IS NOT NULL")
abstract suspend fun getOverrides(): List<MangaPrefsEntity>
@Query("UPDATE preferences SET cf_brightness = 0, cf_contrast = 0, cf_invert = 0, cf_grayscale = 0")
abstract suspend fun resetColorFilters()

View File

@@ -86,6 +86,7 @@ fun MangaTag.toEntity() = TagEntity(
key = key,
source = source.name,
id = "${key}_${source.name}".longHashCode(),
isPinned = false, // for future use
)
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)

View File

@@ -4,9 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_PREFERENCES
@Entity(
tableName = "preferences",
tableName = TABLE_PREFERENCES,
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
@@ -25,4 +26,8 @@ data class MangaPrefsEntity(
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
@ColumnInfo(name = "cf_grayscale") val cfGrayscale: Boolean,
@ColumnInfo(name = "cf_book") val cfBookEffect: Boolean,
@ColumnInfo(name = "title_override") val titleOverride: String?,
@ColumnInfo(name = "cover_override") val coverUrlOverride: String?,
@ColumnInfo(name = "content_rating_override") val contentRatingOverride: String?,
)

View File

@@ -17,4 +17,5 @@ data class MangaSourceEntity(
@ColumnInfo(name = "added_in") val addedIn: Int,
@ColumnInfo(name = "used_at") val lastUsedAt: Long,
@ColumnInfo(name = "pinned") val isPinned: Boolean,
@ColumnInfo(name = "cf_state") val cfState: Int,
)

View File

@@ -12,4 +12,5 @@ data class TagEntity(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "source") val source: String,
@ColumnInfo(name = "pinned") val isPinned: Boolean,
)

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration25To26 : Migration(25, 26) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE sources ADD COLUMN cf_state INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE preferences ADD COLUMN title_override TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE preferences ADD COLUMN cover_override TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE preferences ADD COLUMN content_rating_override TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE favourites ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE tags ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration26To27 : Migration(26, 27) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE preferences ADD COLUMN cf_book INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,9 +1,13 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
class CloudFlareBlockedException(
val url: String,
val source: MangaSource?,
) : IOException("Blocked by CloudFlare")
override val url: String,
source: MangaSource?,
) : CloudFlareException("Blocked by CloudFlare", CloudFlareHelper.PROTECTION_BLOCKED) {
override val source: MangaSource = source ?: UnknownMangaSource
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaSource
abstract class CloudFlareException(
message: String,
val state: Int,
) : IOException(message) {
abstract val url: String
abstract val source: MangaSource
}

View File

@@ -1,11 +1,15 @@
package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
class CloudFlareProtectedException(
val url: String,
val source: MangaSource?,
override val url: String,
source: MangaSource?,
@Transient val headers: Headers,
) : IOException("Protected by CloudFlare")
) : CloudFlareException("Protected by CloudFlare", CloudFlareHelper.PROTECTION_CAPTCHA) {
override val source: MangaSource = source ?: UnknownMangaSource
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.exceptions
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.parsers.model.Manga
class EmptyMangaException(
val reason: EmptyMangaReason?,
val manga: Manga,
cause: Throwable?
) : IllegalStateException(cause)

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