Compare commits

...

612 Commits

Author SHA1 Message Date
Koitharu
5c86de555a Merge branch 'master' into devel 2022-08-23 09:35:16 +03:00
Koitharu
c4e7807d18 Update version 2022-08-23 09:23:34 +03:00
Koitharu
78dd0588ce Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2022-08-22 12:09:16 +03:00
Koitharu
2d73894880 Fix details ui 2022-08-22 12:08:45 +03:00
Zakhar Timoshenko
8ac19e557b Support rounded corners for reader info bar 2022-08-21 15:11:36 +03:00
Koitharu
f677a75ad1 Restore state of ssiv 2022-08-20 09:55:50 +03:00
Koitharu
8e55a4d824 Fix Shikimori authToken refreshing 2022-08-19 11:46:05 +03:00
Koitharu
6b5d8ff0f1 Fix Shikimori authToken refreshing 2022-08-19 11:45:43 +03:00
Koitharu
0c7da53349 Improve details ui 2022-08-19 11:25:36 +03:00
Koitharu
2a3cc11728 Save reader state on idle 2022-08-19 10:22:16 +03:00
Koitharu
a806634bc0 Add text outline to reader info bar 2022-08-19 10:03:13 +03:00
Koitharu
6879d046f8 Adjust reader info bar insets 2022-08-18 17:00:59 +03:00
Koitharu
0248f84ca0 Fix in-app update checking 2022-08-18 16:13:35 +03:00
Koitharu
ff0706dae5 Change acra sender to http 2022-08-18 15:43:12 +03:00
Koitharu
b73fe0398f Highlight new records in feed 2022-08-18 12:33:17 +03:00
Koitharu
f78ae4a818 Make BottomSheet`s handle fit top window inset 2022-08-18 11:35:21 +03:00
Koitharu
072f6d8c69 Show dialog on manga loading error 2022-08-18 11:07:20 +03:00
Koitharu
68b68eb4c5 Handle unknown manga sources 2022-08-18 10:15:34 +03:00
Koitharu
7f95eead50 Merge branch 'master' into devel 2022-08-18 09:18:17 +03:00
Dpper
1935de8f20 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (376 of 376 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-08-18 09:15:50 +03:00
Koitharu
c1e9fde6e8 Update parsers and version 2022-08-17 16:51:03 +03:00
Koitharu
9ded9b84e0 Translated using Weblate (Russian)
Currently translated at 100.0% (376 of 376 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-08-13 16:45:24 +03:00
Neko Nekowazarashi
21037529c0 Translated using Weblate (Indonesian)
Currently translated at 84.0% (316 of 376 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.4% (295 of 376 strings)

Co-authored-by: Neko Nekowazarashi <kodra@nekoweb.my.id>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2022-08-13 16:45:24 +03:00
kuragehime
ec8bdbe6e1 Translated using Weblate (Japanese)
Currently translated at 100.0% (376 of 376 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-08-13 16:45:24 +03:00
Koitharu
b37fd6f4ab Update dependencies 2022-08-13 16:40:21 +03:00
Koitharu
68dcacb918 Fix slider-related crash in reader 2022-08-13 12:50:04 +03:00
Koitharu
49a7408715 Support multiple scrobblers 2022-08-13 12:45:13 +03:00
Koitharu
5abbddba1e Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2022-08-12 07:48:49 +03:00
Koitharu
2382bf1063 Merge branch 'devel' of https://hosted.weblate.org/git/kotatsu/strings into devel 2022-08-12 07:48:17 +03:00
Zakhar Timoshenko
f9b409634b Center crop cover in grid 2022-08-11 22:40:28 +03:00
Koitharu
bb96383d27 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2022-08-11 17:02:30 +03:00
Zakhar Timoshenko
96a7a46981 Translated using Weblate (Russian)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-08-11 15:49:30 +02:00
kuragehime
4b3446ce0e Translated using Weblate (Japanese)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-08-11 15:49:30 +02:00
Sergio Varela
6f302e2536 Translated using Weblate (Spanish)
Currently translated at 100.0% (374 of 374 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-08-11 15:49:30 +02:00
Aliaksiej Razumaŭ
53c326ad05 Translated using Weblate (Belarusian)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2022-08-11 15:49:29 +02:00
Zakhar Timoshenko
7befb88e15 Rename Updates to Feed, Tools to Options 2022-08-11 16:49:01 +03:00
Koitharu
68e8ccf6bd Merge branch 'master' into devel 2022-08-11 16:30:43 +03:00
Koitharu
32e80c7e95 Fix downloads cancellation #210 2022-08-11 15:49:44 +03:00
Koitharu
c07a3b9d0d Download control buttons in list 2022-08-11 12:55:02 +03:00
Koitharu
893d1a881d Fix download service stopping #210 2022-08-11 12:15:24 +03:00
Koitharu
43ef130052 Group download notification 2022-08-11 11:40:27 +03:00
Koitharu
d5bea0ca53 Fix DownloadService leak 2022-08-11 10:18:44 +03:00
Koitharu
9c740c5cc1 Fix settings title 2022-08-10 15:30:57 +03:00
Koitharu
a5b85c296a Update version 2022-08-10 15:17:56 +03:00
Koitharu
2d453bb553 Merge branch 'feature/nextgen' into devel 2022-08-10 15:16:29 +03:00
Koitharu
ad1d247694 Watch local manga directories for changes 2022-08-10 15:15:22 +03:00
lowak
cf7535e2ba Translated using Weblate (Swedish)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: lowak <lowak@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
2022-08-10 15:14:17 +03:00
Koitharu
0077dc2f1c Option to import manga from directories #31 2022-08-10 15:06:10 +03:00
Koitharu
59a50e163f Fix reader slider behavior 2022-08-09 15:22:19 +03:00
Koitharu
22e5b958bc Fix DownloadService leak 2022-08-09 15:15:14 +03:00
Koitharu
905c4b362c Tune ui 2022-08-09 14:56:05 +03:00
Koitharu
6e71f20470 Update readme and screenshots 2022-08-09 12:32:24 +03:00
Koitharu
c326136bdb Adopt FaviconFetcher for unknown manga soures 2022-08-08 16:54:10 +03:00
Koitharu
21c13835e6 Merge branch 'devel' into feature/nextgen 2022-08-08 16:27:21 +03:00
Koitharu
deef3bd8b5 Revert "Fix coil cropping cached images"
This reverts commit 678cef0a45.
2022-08-08 16:22:07 +03:00
Koitharu
87afad29ce Update parsers 2022-08-08 16:11:24 +03:00
Koitharu
436233e735 Fix description text color 2022-08-08 11:49:48 +03:00
Koitharu
6e367ddd74 Failsafe implementation of MangaSource.valueOf 2022-08-08 11:04:35 +03:00
Koitharu
fcdfaf5564 Fix covers size resolving 2022-08-08 10:50:53 +03:00
Zakhar Timoshenko
97884ae25b Remove padding for fast scroller 2022-08-08 04:48:06 +03:00
Zakhar Timoshenko
bc2ffef17e Padding for description 2022-08-08 04:47:43 +03:00
Zakhar Timoshenko
890c13d83a Insets suffering continue 2022-08-08 04:47:05 +03:00
Zakhar Timoshenko
ec8d4b56b5 Move owner interfaces to another directory 2022-08-08 02:45:24 +03:00
Zakhar Timoshenko
99cdfbc07b Fix broken bookmarks screen scrolling 2022-08-08 02:38:14 +03:00
Zakhar Timoshenko
71efde08a8 Refresh action mode colors 2022-08-08 02:36:33 +03:00
Zakhar Timoshenko
678cef0a45 Fix coil cropping cached images 2022-08-07 21:54:26 +03:00
Zakhar Timoshenko
57f1a48602 Using custom sample data 2022-08-07 21:29:37 +03:00
Zakhar Timoshenko
f5eb1619ed Update image placeholders 2022-08-07 14:43:58 +03:00
Zakhar Timoshenko
032ed27c38 Fix landscape UI, change paddings and selectors 2022-08-07 14:41:59 +03:00
Zakhar Timoshenko
78f8407eca Resolve conflicts 2022-08-07 03:04:00 +03:00
Zakhar Timoshenko
9a22001289 Update tablet details info UI 2022-08-07 02:13:51 +03:00
Zakhar Timoshenko
8e08b5003e Fix snackbar in feed screen for tablets 2022-08-06 23:57:44 +03:00
Zakhar Timoshenko
e545c8b897 Update states icons 2022-08-06 20:32:04 +03:00
Zakhar Timoshenko
092a265e6d Some update error message 2022-08-06 20:18:32 +03:00
Zakhar Timoshenko
dc1f494e92 Set anchor to snackbar in details screen 2022-08-06 20:15:48 +03:00
Koitharu
dff17fd11f Change BaseSavedState to AbsSavedState 2022-08-06 18:37:25 +03:00
Zakhar Timoshenko
7b702e98da Swapped rating and state fields on details screen 2022-08-06 17:28:49 +03:00
Zakhar Timoshenko
17d07f3b14 Forgot to add AppBarOwner 2022-08-06 12:08:38 +03:00
Zakhar Timoshenko
a21087ac9b Fix crash on main screen after unloading app from memory 2022-08-05 20:32:47 +03:00
Zakhar Timoshenko
0af1eebd62 Add bottom inset to bookmarks screen 2022-08-05 20:31:08 +03:00
Koitharu
85af73df99 Update dependencies 2022-08-04 13:43:06 +03:00
Koitharu
c7a97711c0 Optimize chapters mapping 2022-08-04 11:59:09 +03:00
Koitharu
ffbe05b2ae Fix tracker for multiple branches 2022-08-04 11:32:50 +03:00
Zakhar Timoshenko
39169d3afe Add bottom inset to feed screen 2022-08-03 01:50:19 +03:00
Zakhar Timoshenko
c89ba12fb5 Fix tracking related crashes 2022-08-02 19:43:07 +03:00
Zakhar Timoshenko
8fdec72f8d Fix status bar color for dark mode 2022-08-02 19:08:00 +03:00
Zakhar Timoshenko
13119c95ac Fix crash when opening pages BS 2022-08-02 17:56:46 +03:00
Koitharu
c70ecd9cfd Information bar in reader 2022-08-02 15:02:26 +03:00
Koitharu
6c43881cf4 Autoscroll in reader #204 2022-08-02 12:40:27 +03:00
Koitharu
14f5d5daa4 Update parsers 2022-08-01 17:14:35 +03:00
Koitharu
f342cd6b56 Fix crash on widgets update 2022-08-01 17:00:00 +03:00
Koitharu
57929f62ad Update downloads activity 2022-08-01 16:34:20 +03:00
Koitharu
523ee1e2a9 Update storage usage indicator 2022-08-01 11:18:34 +03:00
Koitharu
8b0f221eef Refactor MainActivity navigation and AppBars behavior 2022-08-01 10:33:58 +03:00
Koitharu
656405edbc Merge branch 'feature/nextgen' of github.com:KotatsuApp/Kotatsu into feature/nextgen 2022-07-31 18:21:10 +03:00
Koitharu
407c04fe2c Make BottomSheetHeaderBar fixed height 2022-07-31 18:20:52 +03:00
Koitharu
3866d126b7 Merge branch 'devel' into feature/nextgen 2022-07-31 18:00:36 +03:00
Zakhar Timoshenko
bdf836b7d9 Add background source config items 2022-07-31 17:55:13 +03:00
Zakhar Timoshenko
997de528b8 Don't lift appbar in MainActivity 2022-07-31 17:53:19 +03:00
Koitharu
8faacab53a Fix github url 2022-07-30 16:10:03 +03:00
Koitharu
659c327a6d Update parsers and version 2022-07-30 16:05:01 +03:00
Koitharu
bcc2f531c3 Ability to resume download after IOException 2022-07-30 16:02:13 +03:00
Koitharu
020df5c1f7 Fix saving pages from cbz 2022-07-30 14:15:12 +03:00
Koitharu
0d8e4dee35 Update reader mode selection ui 2022-07-30 14:00:08 +03:00
Koitharu
d6781e1d14 Yet another attempt to make webtoon reader great again 2022-07-29 15:39:04 +03:00
Zakhar Timoshenko
c313184666 Remove unnecessary bottom inset in chapters list 2022-07-28 20:34:47 +03:00
Koitharu
ea3b43ba88 Redesign details screen 2022-07-28 17:56:05 +03:00
Koitharu
0eebddb24c Merge branch 'feature/nextgen' of github.com:KotatsuApp/Kotatsu into feature/nextgen 2022-07-28 09:56:50 +03:00
Zakhar Timoshenko
d42cd59880 Fix enabling disabled new sources
Co-authored-by: Koitharu <8948226+nv95@users.noreply.github.com>
2022-07-28 09:49:30 +03:00
Zakhar Timoshenko
6eb859b9f1 Fix enabling disabled new sources
Co-authored-by: Koitharu <8948226+nv95@users.noreply.github.com>
2022-07-27 23:24:41 +03:00
Zakhar Timoshenko
19f1322246 Add remaining entry points 2022-07-27 22:38:08 +03:00
Koitharu
38f45ad483 Merge branch 'devel' into feature/nextgen 2022-07-27 19:32:01 +03:00
Koitharu
7f46ff6d72 Improve app update info 2022-07-27 19:29:28 +03:00
Koitharu
be19c32fea Update prasers 2022-07-27 17:36:39 +03:00
Koitharu
8da0e98d23 Fix FragmentManager leak 2022-07-27 17:36:39 +03:00
Koitharu
73a2f05509 Fix FadingSnackbar text color 2022-07-27 17:36:39 +03:00
Koitharu
bb23f998e0 Fix crash on description selection 2022-07-27 17:36:35 +03:00
TheDawnOvO
75915ff366 Update activity_main.xml 2022-07-27 15:24:39 +03:00
Dpper
517e801580 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
J. Lavoie
12474e23f9 Translated using Weblate (Finnish)
Currently translated at 96.2% (311 of 323 strings)

Translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (320 of 323 strings)

Translated using Weblate (German)

Currently translated at 97.8% (316 of 323 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
kuragehime
00bdd859a7 Translated using Weblate (Japanese)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
Oğuz Ersen
3a3af9ea00 Translated using Weblate (Turkish)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
Koitharu
b9244bd11a Experimental: FlowLiveData 2022-07-26 20:16:47 +03:00
Koitharu
532ec0129a Merge branch 'feature/nextgen_dagger' into feature/nextgen 2022-07-25 21:26:09 +03:00
Zakhar Timoshenko
464e47d6ad Replace z-axis fragment transition to fade 2022-07-25 19:01:08 +03:00
Koitharu
2bbdd3f044 Migrate from Koin to Dagger/Hilt 2022-07-25 17:55:38 +03:00
Koitharu
0757a31381 Merge branch 'devel' into feature/nextgen 2022-07-25 11:34:53 +03:00
Koitharu
1803b1a2ee Remove unused WebViewClientCompat 2022-07-25 11:25:43 +03:00
Koitharu
36634414bc Bottom sheet header bar view 2022-07-24 18:36:53 +03:00
Koitharu
802448cb5a Fix grid size computing 2022-07-24 18:36:53 +03:00
Zakhar Timoshenko
8ab9b4d1c3 Fix not collapsing appbar on some screens when dragging scroller 2022-07-24 12:58:58 +03:00
Koitharu
02fa33597a Fix chip checked icon color 2022-07-24 11:07:46 +03:00
Koitharu
5edfda6c1a Manage library grid size 2022-07-24 10:42:52 +03:00
Koitharu
5a565a16fe Fix build 2022-07-24 09:08:01 +03:00
Zakhar Timoshenko
652ef7ee51 Fix wrong background 2022-07-23 01:31:26 +03:00
Zakhar Timoshenko
fb47480fba Fix slider +1 page bug (coolest bugfix) 2022-07-23 00:00:32 +03:00
Zakhar Timoshenko
fc3ec644b3 Fix unreachable code (god I hope this will not break updates) 2022-07-22 23:55:56 +03:00
Zakhar Timoshenko
9f21f5900f Collapsing toolbar layout (?) 2022-07-22 23:52:22 +03:00
Zakhar Timoshenko
11710d36d1 Update BS UI 2022-07-22 23:41:06 +03:00
Zakhar Timoshenko
a975ab58ee Fix corrupted discord icon on older Android versions 2022-07-22 23:39:50 +03:00
Koitharu
1306882377 Merge branch 'feature/nextgen' of github.com:KotatsuApp/Kotatsu into feature/nextgen 2022-07-22 19:30:53 +03:00
Zakhar Timoshenko
b555e24fef Merge branch 'devel' into feature/nextgen
# Conflicts:
#	app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt
#	app/src/main/res/menu/opt_categories.xml
2022-07-22 19:16:45 +03:00
Koitharu
b8250d5e44 Merge branch 'devel' into feature/nextgen 2022-07-22 19:11:04 +03:00
Koitharu
4175c84363 Move create category button in bs to toolbar 2022-07-22 19:04:43 +03:00
Zakhar Timoshenko
9917787d6f Return maxLines, but value a bit more 2022-07-22 18:27:02 +03:00
Zakhar Timoshenko
d189e09aba Use collapsing toolbar layout for tablets 2022-07-22 18:20:23 +03:00
Zakhar Timoshenko
7ff20245b3 Unlimited manga title in details screen 2022-07-22 18:19:55 +03:00
Zakhar Timoshenko
da1696a059 Don't recreate the same fragment 2022-07-22 18:18:40 +03:00
Koitharu
e6202103bc Adjust library sublist scroll on changes 2022-07-22 14:41:34 +03:00
Koitharu
7c659371a9 Remove from library action 2022-07-22 13:13:00 +03:00
Zakhar Timoshenko
83886362be Rerevert new reader bs 2022-07-22 08:24:06 +03:00
Zakhar Timoshenko
70de4f750c Attempt to fix insets 2022-07-21 23:14:40 +03:00
Zakhar Timoshenko
af901baff3 Update animated icons 2022-07-21 21:11:15 +03:00
Koitharu
d69f4bbcaf Fix library categories 2022-07-21 19:39:33 +03:00
Koitharu
089e3dc209 Update in-app update checking 2022-07-21 19:32:32 +03:00
Koitharu
c158c4e18e Implement incognito mode switching 2022-07-21 17:26:58 +03:00
Koitharu
300d365d8b Update library fragment 2022-07-21 17:14:03 +03:00
Koitharu
81df005655 Test restoring an old backup 2022-07-21 12:49:40 +03:00
Koitharu
feb19c4eb5 Fix favourites and history deletion 2022-07-21 11:15:20 +03:00
Zakhar Timoshenko
9360787897 Revert color scheme 2022-07-21 08:13:36 +03:00
Zakhar Timoshenko
ac79557e22 Fix merging artifacts attempt 2 2022-07-21 08:11:58 +03:00
Zakhar Timoshenko
c9695b1d2f synchronization info layout maybe? 2022-07-21 00:22:45 +03:00
Zakhar Timoshenko
27177996d3 Animated icons 2022-07-21 00:06:46 +03:00
Zakhar Timoshenko
2306330fd0 Use coverUrl instead of largeCoverUrl in lists 2022-07-21 00:06:24 +03:00
Zakhar Timoshenko
0d1e85d0c2 Fix merging troubles 2022-07-21 00:05:57 +03:00
Zakhar Timoshenko
3b5a305122 Merge branch 'devel' into feature/nextgen
# Conflicts:
#	app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
#	app/src/main/res/values/strings.xml
2022-07-20 21:39:43 +03:00
Koitharu
1840d7b50e Fix get current page #165 2022-07-20 20:32:01 +03:00
Zakhar Timoshenko
37b69833b3 Update parsers 2022-07-20 19:52:56 +03:00
Dpper
093f766d1d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-07-20 18:34:50 +03:00
Sergio Varela
69d8459b1c Translated using Weblate (Spanish)
Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-07-20 18:34:50 +03:00
kuragehime
fa8a526642 Translated using Weblate (Japanese)
Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-07-20 18:34:50 +03:00
Oğuz Ersen
1d35d951e6 Translated using Weblate (Turkish)
Currently translated at 100.0% (322 of 322 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-07-20 18:34:50 +03:00
J. Lavoie
3c0420f42f Translated using Weblate (Finnish)
Currently translated at 96.2% (310 of 322 strings)

Translated using Weblate (French)

Currently translated at 100.0% (322 of 322 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (319 of 322 strings)

Translated using Weblate (German)

Currently translated at 97.8% (315 of 322 strings)

Translated using Weblate (French)

Currently translated at 100.0% (321 of 321 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (319 of 321 strings)

Translated using Weblate (German)

Currently translated at 97.8% (314 of 321 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-07-20 18:34:50 +03:00
Koitharu
d000a825d3 Update parsers 2022-07-20 18:32:38 +03:00
Koitharu
23b28672d4 Fix sharing files from sdcard 2022-07-20 18:03:46 +03:00
Koitharu
a076c9f420 Make NotFoundException resolvable 2022-07-20 17:32:22 +03:00
Koitharu
e1db294b07 Fix favourites adding/removing 2022-07-20 15:28:54 +03:00
Koitharu
2004c3a7d5 Fix save page request leak 2022-07-20 15:19:56 +03:00
Koitharu
44ac5270e0 Merge branch 'devel' into feature/nextgen 2022-07-20 15:19:06 +03:00
Koitharu
bdc7a8f5ed Fallback to system dnf if DoH failed 2022-07-20 15:13:45 +03:00
Koitharu
f9baa4a8ad Reader options bs 2022-07-20 15:03:17 +03:00
Koitharu
9c94a273ea Slider in reader #204 2022-07-20 14:09:44 +03:00
Koitharu
efffbab4a7 Fix empty categories 2022-07-20 08:33:13 +03:00
Koitharu
7b53468c2e Add missing empty states 2022-07-19 14:49:34 +03:00
Koitharu
59243be030 Sync fixes 2022-07-19 14:33:21 +03:00
Koitharu
57c1d070d1 Merge branch 'feature/sync' into feature/nextgen 2022-07-19 12:57:38 +03:00
Koitharu
5b2f2b0fd7 Merge branch 'devel' into feature/nextgen 2022-07-19 12:09:23 +03:00
Koitharu
bdcc3bb1f5 Update parsers and adjust ParseException reporting 2022-07-19 12:07:21 +03:00
Koitharu
18d45aa1a3 Chapters range selection 2022-07-19 10:29:39 +03:00
Koitharu
b5bb8efe0a Improve database tests 2022-07-19 10:01:23 +03:00
Koitharu
f18c18230b Update favourites categories empty state image 2022-07-18 18:29:29 +03:00
Koitharu
1a83d21410 Cleanup after merging 2022-07-18 18:19:51 +03:00
Koitharu
094cebe674 Merge branch 'feature/nextgen' into feature/sync 2022-07-18 15:09:55 +03:00
Koitharu
3963099053 GZip compression for networking 2022-07-18 14:37:30 +03:00
Koitharu
f3181cc0f1 Merge branch 'devel' into feature/nextgen 2022-07-18 14:30:59 +03:00
Koitharu
2fd1e998f4 Fix unmark chapter as new 2022-07-18 14:30:07 +03:00
Koitharu
c5a1980e0d Fix tests for api21 2022-07-18 14:12:24 +03:00
Koitharu
008863fee8 Merge branch 'devel' into feature/nextgen 2022-07-18 13:49:48 +03:00
Koitharu
d470ca4b47 Test for update checking 2022-07-18 13:09:11 +03:00
Koitharu
35f450e444 Fix android tests 2022-07-18 12:57:41 +03:00
Koitharu
206fb4e584 Update test data 2022-07-18 12:42:15 +03:00
Koitharu
62088b36a4 Update app shortcuts using InvalidationTracker.Callback 2022-07-18 12:16:39 +03:00
Koitharu
aa5fd530d3 Fix database test 2022-07-18 11:34:00 +03:00
Koitharu
f0ee64bafa Unit test for BackupAgent 2022-07-18 11:27:56 +03:00
Koitharu
dfa413da6f Observe database updates using InvalidationTracker 2022-07-18 09:55:49 +03:00
Koitharu
9eb5e699e1 Unit tests for json (de)serialization 2022-07-18 09:19:29 +03:00
Zakhar Timoshenko
1927008c2e Actualize app colors + revert old accent color 2022-07-16 18:36:11 +03:00
Zakhar Timoshenko
5d0dac6947 Some cleanup 2022-07-16 17:42:35 +03:00
Zakhar Timoshenko
71351ad701 [wip] Initial add incognito mode 2022-07-15 00:10:11 +03:00
Zakhar Timoshenko
abf5c8fb3c Revert stuff after merging branch 2022-07-14 23:18:21 +03:00
Zakhar Timoshenko
865311d864 Merge remote-tracking branch 'origin/feature/nextgen' into feature/nextgen 2022-07-14 23:08:34 +03:00
Zakhar Timoshenko
4e8b8e0bc2 Merge branch 'devel' into feature/nextgen
# Conflicts:
#	app/build.gradle
2022-07-14 23:07:12 +03:00
Koitharu
c0fbf846bd Merge branch 'devel' into feature/nextgen 2022-07-14 15:03:13 +03:00
Koitharu
2d4c1b751e Update dependencies 2022-07-14 14:48:26 +03:00
Koitharu
91b17ef4a2 Fix global search parallelism 2022-07-14 14:23:04 +03:00
Koitharu
9b748f7334 Update parsers and version 2022-07-14 14:12:47 +03:00
Zakhar Timoshenko
517f2bcee7 Tweak on hide bottom navigation behavior 2022-07-14 00:08:20 +03:00
Koitharu
4607e5ba0f Refactor LiveData extensions 2022-07-13 15:58:20 +03:00
Koitharu
29c82177a9 Change package name for "next" branch 2022-07-13 15:49:23 +03:00
Koitharu
69a0c779d9 Merge branch 'devel' into feature/nextgen 2022-07-13 14:53:00 +03:00
Koitharu
2deaed2067 Optimize image loading in lists 2022-07-13 14:48:33 +03:00
Koitharu
3be9def609 Add storage usage to Tools screen 2022-07-13 14:25:08 +03:00
Koitharu
bd3d800cde Fix reading progress indicator position 2022-07-13 11:39:13 +03:00
Koitharu
cef2449d45 Merge branch 'devel' into feature/nextgen 2022-07-13 11:36:15 +03:00
Koitharu
8d894c97f3 Update shelf widget items ui 2022-07-13 11:36:08 +03:00
Koitharu
0e1b5b19d2 Optimize image loading in lists 2022-07-13 11:24:20 +03:00
Koitharu
2595c11686 Tune favourite categories list ui 2022-07-13 10:43:51 +03:00
Koitharu
334e08730e Option to enable exit confirmation 2022-07-13 10:31:08 +03:00
Zakhar Timoshenko
d6ff996dbe Fix not collapsing/expanding toolbar when scrolling vertically over horizontal RecyclerView 2022-07-12 21:44:07 +03:00
Zakhar Timoshenko
3040b89a9e Fix crash on first app initialization 2022-07-12 21:41:35 +03:00
Zakhar Timoshenko
aed180c845 Harmonize background color in favicon fallback 2022-07-12 19:28:52 +03:00
Zakhar Timoshenko
4ba1e2f661 Remove unnecessary padding 2022-07-12 19:14:33 +03:00
Zakhar Timoshenko
8b3ef3a3f3 Confirming exit from app 2022-07-12 19:14:12 +03:00
Koitharu
b293fee742 Move favourites order from menu to header 2022-07-12 14:34:08 +03:00
Koitharu
fb608ed30a Fix findProgress function not suspend 2022-07-12 12:49:49 +03:00
Koitharu
2654de96ba Configure visible categories in library 2022-07-12 12:48:50 +03:00
Koitharu
8e43afe408 Prohibit empty names in favourite categories 2022-07-12 12:02:49 +03:00
Koitharu
e2aea345d4 Prohibit empty names in favourite categories 2022-07-12 12:02:36 +03:00
Koitharu
78295898cb Search by single source 2022-07-12 11:56:18 +03:00
Koitharu
4fcdd9f370 Merge branch 'devel' into feature/nextgen 2022-07-12 11:27:20 +03:00
Koitharu
73df680214 Add domain validation in source settings 2022-07-12 11:13:33 +03:00
Koitharu
fa4aa154a3 Update parsers and version 2022-07-12 10:53:23 +03:00
Koitharu
cf7cdbc41b Fix CloudFlare resolve url 2022-07-12 10:46:45 +03:00
Koitharu
c2561a1de0 Fix CompositeMutex concurrent locking 2022-07-12 10:46:45 +03:00
Koitharu
a36abe0272 Fix missed git conflicts 2022-07-12 10:46:44 +03:00
Koitharu
5b10d697f6 Fix ReadActivity error reporting 2022-07-12 10:46:44 +03:00
Koitharu
e0f07ccc3b Update issue templates 2022-07-12 10:43:38 +03:00
DevCoz
938ea8fb73 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.7% (316 of 320 strings)

Co-authored-by: DevCoz <qq851428876@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-07-12 10:43:28 +03:00
Sergio Varela
ea6a338128 Translated using Weblate (Spanish)
Currently translated at 100.0% (320 of 320 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-07-12 10:43:28 +03:00
Koitharu
9c66f74a5b Parse favicons in runtime 2022-07-12 09:38:47 +03:00
Zakhar Timoshenko
9dc3ad38fc Some manga items styling 2022-07-11 21:51:39 +03:00
Zakhar Timoshenko
9599cdd2f6 Fix crash 2022-07-11 18:53:35 +03:00
Zakhar Timoshenko
4e5becb647 Tweak displaying cached covers 2022-07-10 20:23:08 +03:00
Zakhar Timoshenko
1d15a64945 Merge branch 'devel' into feature/nextgen
# Conflicts:
#	app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt
2022-07-10 12:55:52 +03:00
Zakhar Timoshenko
21891d2d9d Tweak collapsing toolbars 2022-07-10 12:51:48 +03:00
Koitharu
5b066c9dde Tune ui 2022-07-09 18:42:33 +03:00
Koitharu
e4668cf938 Merge branch 'feature/nextgen' of github.com:KotatsuApp/Kotatsu into feature/nextgen 2022-07-09 18:37:27 +03:00
Koitharu
3978ebc230 Tools fragment 2022-07-09 18:37:10 +03:00
Koitharu
4402db33fd Tune transitions 2022-07-09 17:58:45 +03:00
Zakhar Timoshenko
3511c02c69 Tweak cover stack in category item 2022-07-09 17:25:22 +03:00
Koitharu
12be24c050 Don`t show FastScroller if list is no scrollable 2022-07-09 17:24:27 +03:00
Koitharu
ce3a668103 Fix category edit activity recreation 2022-07-09 16:59:12 +03:00
Koitharu
80db7f0b74 Selection and reodering favourites categories 2022-07-09 16:52:55 +03:00
Koitharu
451b9fc0f1 Fix FastScroller & SwipeRefreshLayout behavior 2022-07-09 14:44:19 +03:00
Koitharu
e2ed7f0d77 Refactor and tune FastScroller 2022-07-09 14:34:42 +03:00
Koitharu
4743f40154 Refactor animation duraton 2022-07-09 10:56:37 +03:00
Koitharu
53f127987c Always suggest available tags in manga list header 2022-07-09 09:13:48 +03:00
Koitharu
f6c6111459 Fix tabs on api<23 2022-07-09 08:56:21 +03:00
Koitharu
f5dd1c39ce Refactor 2022-07-09 08:51:52 +03:00
Zakhar Timoshenko
b519b53419 Add more improved fast scroller 2022-07-09 00:26:05 +03:00
Koitharu
c5de765e52 Favourites categories screen 2022-07-08 17:16:47 +03:00
Koitharu
1381a7d957 Random manga dice 2022-07-08 15:29:51 +03:00
Koitharu
602a5eb2ab Update manga list header 2022-07-08 14:57:53 +03:00
Koitharu
7d41318d15 Add manga sources to search suggestion 2022-07-08 13:57:58 +03:00
Koitharu
dd8cb8dfd0 Merge branch 'devel' into feature/nextgen 2022-07-08 12:51:13 +03:00
Koitharu
557c2b018a Update version and parsers 2022-07-08 12:33:05 +03:00
Koitharu
3add01d57e Improve reporting of caught exceptions 2022-07-08 10:35:18 +03:00
Koitharu
2ad1ea98f1 Fix crash in SourceSettingsFragment 2022-07-08 10:27:11 +03:00
Zakhar Timoshenko
cfc317cf19 Misc changes 2022-07-08 07:53:38 +03:00
Koitharu
242704f853 Improve explore screen 2022-07-07 17:50:05 +03:00
Koitharu
6df56c2d77 Bookmarks screen 2022-07-07 16:52:34 +03:00
Koitharu
49634a2f52 Improve explore fragment 2022-07-07 14:28:06 +03:00
Koitharu
b7442fe445 Merge branch 'devel' into feature/nextgen 2022-07-07 12:07:11 +03:00
Koitharu
3121532217 Translated using Weblate (Russian)
Currently translated at 100.0% (320 of 320 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-07-07 11:35:56 +03:00
Koitharu
20ac12ca0d Update readme and metadata 2022-07-07 11:06:20 +03:00
Koitharu
f0b222140e Update parsers 2022-07-07 10:17:58 +03:00
Koitharu
2a35ca6094 Fix landscape details view #197 2022-07-07 09:14:50 +03:00
Zakhar Timoshenko
b9428e3898 Initial adding Explore screen 2022-07-07 00:19:51 +03:00
Koitharu
93f9636916 Improve webtoon reader performance 2022-07-06 19:43:43 +03:00
Koitharu
6a8a6a08db Merge branch 'devel' into feature/nextgen 2022-07-06 16:15:37 +03:00
Koitharu
2c24aba558 Refactor update checker 2022-07-06 16:15:25 +03:00
Koitharu
a35d7dc5ae Ability to resolve errors in manga source settings 2022-07-06 15:13:54 +03:00
J. Lavoie
1e9e7e4cd7 Translated using Weblate (Finnish)
Currently translated at 96.5% (309 of 320 strings)

Translated using Weblate (French)

Currently translated at 100.0% (320 of 320 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (318 of 320 strings)

Translated using Weblate (German)

Currently translated at 97.8% (313 of 320 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-07-06 15:12:08 +03:00
kuragehime
cfd97ebd3d Translated using Weblate (Japanese)
Currently translated at 100.0% (320 of 320 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-07-06 13:12:47 +03:00
Oğuz Ersen
3ac8dc5558 Translated using Weblate (Turkish)
Currently translated at 100.0% (320 of 320 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (319 of 319 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-07-06 13:12:47 +03:00
Anupam Malhotra
6f93440b11 Translated using Weblate (Spanish)
Currently translated at 95.2% (304 of 319 strings)

Co-authored-by: Anupam Malhotra <anpm.malhotra@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-07-06 13:12:47 +03:00
Koitharu
9283f419ba Support images in manga description 2022-07-06 13:12:58 +03:00
Koitharu
be67b36b6a Refactor extensions 2022-07-06 12:49:21 +03:00
Koitharu
6934daecff Preselect new sources based on locale 2022-07-06 12:20:48 +03:00
Koitharu
3d74d027c1 Remove duplicate item from history settings 2022-07-06 12:13:27 +03:00
Koitharu
e048235dad Update dependencies 2022-07-06 12:12:52 +03:00
Koitharu
d21dff08b8 Invalidate DiskLruCache if broken 2022-07-06 11:59:52 +03:00
Koitharu
c4b03d1316 Library menu 2022-07-06 11:50:43 +03:00
Zakhar Timoshenko
5d54298a22 Lift app bar when search opened 2022-07-05 02:48:03 +03:00
Zakhar Timoshenko
11e9f1749a Some lifting magic (when tabs visible != app bar lifted) 2022-07-05 02:45:23 +03:00
Zakhar Timoshenko
09105152e4 Replace CoordinatorLayout in some places :trollface: 2022-07-04 23:16:55 +03:00
Zakhar Timoshenko
a2b8cfe512 Add "Continue" FAB to navigation rail 2022-07-04 22:10:15 +03:00
Koitharu
f42f244443 Multiple selection in library 2022-07-04 18:18:08 +03:00
Koitharu
b81aeaebd3 Fix search opening/closing 2022-07-04 16:56:54 +03:00
Koitharu
e0d93b0630 Improve library list 2022-07-04 16:50:31 +03:00
Koitharu
e4a2897731 Merge branch 'devel' into feature/nextgen 2022-07-04 15:40:49 +03:00
Koitharu
5f06c4c3c0 Fix selection state restoring 2022-07-04 14:21:19 +03:00
Koitharu
b693b34fe7 Add "Show all" button to search results 2022-07-04 14:05:23 +03:00
Koitharu
b7f469957c TrimTransformation for images 2022-07-04 13:48:51 +03:00
Koitharu
4a7b415635 Fix search suggestions manga ShapeAppearance 2022-07-04 13:08:15 +03:00
Koitharu
d9985d03ab Save state of selection in lists 2022-07-04 13:03:11 +03:00
Koitharu
eda72128da Navigation rail on landscape 2022-07-04 10:47:20 +03:00
Koitharu
ebdc2dfb0e Add library fragment 2022-07-04 09:38:39 +03:00
Zakhar Timoshenko
0eff85dca3 Add themed button for Explore screen 2022-07-04 01:15:59 +03:00
Zakhar Timoshenko
da5796b563 Hide bottom nav when search is opened 2022-07-03 21:23:58 +03:00
Zakhar Timoshenko
310d4e58bb Set status bar foreground when app bar is hidden 2022-07-03 20:15:34 +03:00
Zakhar Timoshenko
455351e3a8 Add some animated icons to bottom nav 2022-07-03 20:11:40 +03:00
Zakhar Timoshenko
b4f2b82a0d Merge branch 'devel' into feature/nextgen
# Conflicts:
#	app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt
2022-07-03 12:09:42 +03:00
Zakhar Timoshenko
0e7960fced Initial adding bottom navigation 2022-07-02 22:13:31 +03:00
Koitharu
6e324fd5ab Merge branch 'feature/percent' into devel 2022-07-02 14:51:50 +03:00
Koitharu
167498dd2c Merge branch 'feature/empty_states' into devel 2022-07-02 14:48:31 +03:00
Koitharu
688eaf6aab Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2022-07-02 14:47:53 +03:00
kuragehime
d7df105e04 Translated using Weblate (Japanese)
Currently translated at 100.0% (305 of 305 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-07-02 14:45:55 +03:00
Oğuz Ersen
ea5c4cd027 Translated using Weblate (Turkish)
Currently translated at 100.0% (305 of 305 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-07-02 14:45:55 +03:00
Aliaksiej Razumaŭ
9589706df9 Translated using Weblate (Belarusian)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-07-02 14:45:55 +03:00
Koitharu
e6e37aec47 Merge branch 'devel' into feature/shikimori 2022-07-02 14:39:03 +03:00
Koitharu
fc2d5fe00e Update ScrobblingInfoBottomSheet 2022-07-02 14:33:16 +03:00
Zakhar Timoshenko
a73d3d375a Unregistering scrobbler 2022-07-02 12:33:23 +03:00
Koitharu
ce97c8f7d9 Ability to report non-fatal exceptions 2022-07-02 11:02:03 +03:00
Koitharu
a48abc56dd Catch HttpException in widgets as well 2022-07-01 16:02:33 +03:00
Koitharu
1044d7a8d1 Show checkmark on reading indicator if full 2022-07-01 15:32:47 +03:00
Zakhar Timoshenko
1d2584001f Starting point 2022-07-01 00:13:03 +03:00
Zakhar Timoshenko
fb0a075c50 Fix acknowledgment 2022-06-30 18:42:53 +03:00
Zakhar Timoshenko
8a8c785a31 Add issue moderator action 2022-06-30 18:40:31 +03:00
Zakhar Timoshenko
4e976fc4ec Merge remote-tracking branch 'origin/devel' into devel 2022-06-30 18:24:56 +03:00
Zakhar Timoshenko
cdf06578c1 [Issue template] Add old version of issue templates
This is need for some GitHub clients like Octodroid and something else
2022-06-30 18:24:44 +03:00
Koitharu
4a131d6215 Update empty favourites image 2022-06-30 09:03:34 +03:00
Koitharu
45f71cdcc1 Empty status vector images 2022-06-29 16:43:25 +03:00
Koitharu
5192175ddc Merge branch 'devel' 2022-06-29 12:22:37 +03:00
Koitharu
274a672637 Increase version 2022-06-29 12:21:44 +03:00
Koitharu
c281ab5a39 Fix widgets size and background 2022-06-29 12:01:59 +03:00
Koitharu
0106afc93c Fix warnings 2022-06-29 11:38:33 +03:00
Koitharu
d69b091858 Translated using Weblate (Russian)
Currently translated at 99.6% (303 of 304 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-06-29 11:01:05 +03:00
Koitharu
f71274d90d Update translations 2022-06-29 10:54:17 +03:00
kuragehime
a37b9a1036 Translated using Weblate (Japanese)
Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-06-29 10:35:18 +03:00
Oğuz Ersen
a9b8174b10 Translated using Weblate (Turkish)
Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-06-29 10:35:18 +03:00
J. Lavoie
20a0eda5db Translated using Weblate (Finnish)
Currently translated at 99.6% (303 of 304 strings)

Translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (304 of 304 strings)

Translated using Weblate (German)

Currently translated at 100.0% (304 of 304 strings)

Translated using Weblate (Spanish)

Currently translated at 99.3% (302 of 304 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-06-29 10:35:18 +03:00
Artem
e231dba0b0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (301 of 301 strings)

Co-authored-by: Artem <artem@molotov.work>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-06-29 10:35:18 +03:00
Koitharu
d9459dc8fa Ui adjustments 2022-06-28 12:44:59 +03:00
Koitharu
a55ff5ce5a Option to disable reading progress indicators 2022-06-28 12:35:55 +03:00
Koitharu
f2ea1cde46 Reading progress indicator on details screen 2022-06-28 12:10:39 +03:00
Koitharu
04dd8003f7 Show reading progress indicators in lists 2022-06-28 11:39:54 +03:00
Koitharu
b82b46f7d7 Save reading percent to database 2022-06-27 16:21:53 +03:00
Koitharu
33bccd10fe Update widgets for Android 12 2022-06-27 14:44:43 +03:00
Koitharu
9956f1ae4f Fix detailed list item 2022-06-27 13:11:08 +03:00
Koitharu
09a3c5da23 Update dialogs 2022-06-27 12:26:23 +03:00
Koitharu
b1d51bbefb Migrate to MaterialSwitch component 2022-06-27 12:11:22 +03:00
Koitharu
9e7aaa6c91 Option to disable fingerprint authentication 2022-06-27 11:54:33 +03:00
Koitharu
9686c97731 Add tracker settings to option menu 2022-06-26 11:21:47 +03:00
Koitharu
663718ece4 Add ScrollView to list mode dialog 2022-06-26 11:09:58 +03:00
Koitharu
e3048d1eb2 Compact chapters list 2022-06-26 10:21:47 +03:00
Koitharu
c6785bfda0 Merge branch 'devel' into feature/shikimori 2022-06-26 10:13:57 +03:00
kuragehime
781438d83a Translated using Weblate (Japanese)
Currently translated at 100.0% (301 of 301 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-06-25 20:10:32 +02:00
Oğuz Ersen
f5625cc6b9 Translated using Weblate (Turkish)
Currently translated at 100.0% (301 of 301 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-06-25 20:10:32 +02:00
Allan Nordhøy
132d375d81 Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.6% (279 of 301 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-06-25 20:10:32 +02:00
J. Lavoie
54007ecd28 Translated using Weblate (Finnish)
Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (300 of 301 strings)

Translated using Weblate (French)

Currently translated at 100.0% (301 of 301 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (301 of 301 strings)

Translated using Weblate (German)

Currently translated at 100.0% (301 of 301 strings)

Translated using Weblate (Spanish)

Currently translated at 99.3% (299 of 301 strings)

Added translation using Weblate (Finnish)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-06-25 20:10:31 +02:00
Koitharu
03c2b55776 Replace done menu item with button 2022-06-25 21:11:24 +03:00
Koitharu
c6e46384f8 Fix switches on Lollipop 2022-06-25 21:11:24 +03:00
Zakhar Timoshenko
ce1c607f05 [Issue template] Update 2022-06-24 23:12:24 +03:00
DevCoz
19bbe51f01 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.0% (297 of 300 strings)

Co-authored-by: DevCoz <qq851428876@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-06-24 14:37:15 +03:00
Artem
ff4a4eaf78 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (300 of 300 strings)

Co-authored-by: Artem <artem@molotov.work>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-06-24 14:37:15 +03:00
Koitharu
2cf08f74db Option to disable all sources 2022-06-24 14:10:51 +03:00
Koitharu
cc6bbc9869 Change defult list mode to grid 2022-06-24 13:56:27 +03:00
Koitharu
8d962516f4 Update dependencies 2022-06-24 13:46:18 +03:00
Koitharu
ced4ca01cb Add icon to metadata 2022-06-23 16:04:46 +03:00
Koitharu
1195323acd Add icon to metadata 2022-06-23 14:54:30 +03:00
Koitharu
87b62aef70 Merge branch 'feature/shikimori' of github.com:SkyfaceD/Kotatsu into feature/shikimori 2022-06-22 14:53:12 +03:00
Koitharu
ec89ba0155 Shikimori interaction implementation 2022-06-22 13:24:07 +03:00
Koitharu
0695103589 Merge branch 'devel' into feature/shikimori 2022-06-21 13:31:44 +03:00
Koitharu
8d47b09e80 Add biometric authentication 2022-06-21 12:51:24 +03:00
J. Lavoie
3cd156ae42 Translated using Weblate (Finnish)
Currently translated at 99.3% (298 of 300 strings)

Translated using Weblate (French)

Currently translated at 100.0% (300 of 300 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (300 of 300 strings)

Translated using Weblate (German)

Currently translated at 100.0% (300 of 300 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (297 of 298 strings)

Translated using Weblate (French)

Currently translated at 100.0% (298 of 298 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (298 of 298 strings)

Translated using Weblate (German)

Currently translated at 100.0% (298 of 298 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Luiz-bro
4a22f62ec5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (298 of 298 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Oğuz Ersen
9ad37b4412 Translated using Weblate (Turkish)
Currently translated at 100.0% (300 of 300 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (298 of 298 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
kuragehime
9fcabcb05e Translated using Weblate (Japanese)
Currently translated at 100.0% (300 of 300 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (298 of 298 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Artem
db1ae6020d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (298 of 298 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (296 of 296 strings)

Co-authored-by: Artem <artem@molotov.work>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Tsukino Nami
7e039c9055 Translated using Weblate (Chinese (Simplified))
Currently translated at 3.7% (11 of 296 strings)

Added translation using Weblate (Chinese (Simplified))

Co-authored-by: Tsukino Nami <zhangyongyi666@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
I. Musthafa
db7482ff12 Translated using Weblate (Indonesian)
Currently translated at 94.2% (279 of 296 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.5% (274 of 296 strings)

Translated using Weblate (Indonesian)

Currently translated at 26.3% (78 of 296 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Indonesian)

Added translation using Weblate (Indonesian)

Co-authored-by: I. Musthafa <i.musthafa66@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
2022-06-21 11:01:38 +03:00
Dpper
d63ae7cadd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (296 of 296 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Sergio Varela
c4fa0d405e Translated using Weblate (Spanish)
Currently translated at 100.0% (296 of 296 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-06-21 11:01:38 +03:00
Koitharu
63fef3ab7c Fix empty chapters placeholder #176 2022-06-21 11:00:18 +03:00
Koitharu
7019c07a6d Update parsers 2022-06-21 10:42:25 +03:00
Koitharu
006ea9844a Update readme 2022-06-21 10:40:11 +03:00
Koitharu
d2bbfe01f1 Add ACRA for crash reports 2022-06-20 10:53:58 +03:00
Koitharu
86d8ff3c68 Refactor AboutLinksPreference 2022-06-18 20:16:49 +03:00
Koitharu
00dacc32df Update parsers 2022-06-18 19:51:13 +03:00
Zakhar Timoshenko
1c1bd9265e Fix links 2022-06-18 17:04:01 +03:00
Zakhar Timoshenko
0bb090eee6 Tweak About view 2022-06-18 17:01:12 +03:00
Koitharu
d2f3bfb2e3 Add ignore battery optimization option 2022-06-18 13:11:14 +03:00
Koitharu
634ce0dddf Tracker tests and fixes 2022-06-18 10:59:01 +03:00
Koitharu
c82bacb037 Refactor tracker and add tests 2022-06-16 15:26:57 +03:00
Koitharu
3edfd0892a Move tracker logic into own class 2022-06-15 13:53:29 +03:00
Koitharu
30c0fd600f Fix global search results order 2022-05-31 16:43:23 +03:00
Koitharu
ccb31de1ba Fix wrong tracker notifications 2022-05-31 16:22:30 +03:00
Koitharu
a74b623c10 Refactor menu providers 2022-05-30 15:45:29 +03:00
Koitharu
5808e8f321 Update dependencies 2022-05-30 13:05:26 +03:00
Koitharu
4f3fef3bfe Update parsers 2022-05-25 10:04:27 +03:00
Zakhar Timoshenko
0c07e649bf [Issue template] Update version 2022-05-21 01:32:27 +03:00
Koitharu
fc2820ec11 Update version to v3.3 2022-05-20 19:12:58 +03:00
Zakhar Timoshenko
312fb033e0 Fix weird toolbar in category edit activity 2022-05-20 18:40:53 +03:00
Koitharu
18bc4dc739 Merge branch 'devel' of github.com:nv95/Kotatsu into devel 2022-05-20 12:16:22 +03:00
Artem
2b61b27271 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Co-authored-by: Artem <artem@molotov.work>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-05-20 12:15:03 +03:00
Koitharu
58c9f75b91 Fix tags order in filter 2022-05-20 12:14:23 +03:00
Koitharu
790f1fb8a3 Update parsers 2022-05-20 12:06:26 +03:00
Zakhar Timoshenko
5c4f3f7fe4 Revert onCreateDialog method
This thing is really needed
2022-05-18 23:35:58 +03:00
Zakhar Timoshenko
86ead09080 Revert accidentally removed style 2022-05-18 22:20:54 +03:00
Zakhar Timoshenko
0932507346 Bottom sheet improvements 2022-05-18 22:14:14 +03:00
Koitharu
21f7b7120a Use collator for tags sorting 2022-05-18 11:27:07 +03:00
Koitharu
473135bfc5 Apply theme changing without restarting 2022-05-17 16:26:04 +03:00
Koitharu
ce7960e5e9 Recreate all activities on theme changed 2022-05-17 13:23:03 +03:00
SkyfaceD
3be96cf035 Hide shikimori sensitive information 2022-05-17 15:40:34 +06:00
Koitharu
17c440ee43 Fix marking sources as new 2022-05-17 11:31:06 +03:00
Koitharu
5d881ca154 New global search activity 2022-05-17 11:19:55 +03:00
Koitharu
d5216e3784 Fix sync settings intent 2022-05-16 17:14:07 +03:00
Koitharu
bd29c64370 Gc database 2022-05-16 15:07:34 +03:00
Koitharu
a9f3ab259a Schedule sync with rate limit 2022-05-16 10:36:36 +03:00
Koitharu
318486d62b Undo deletion from favourites 2022-05-15 20:10:56 +03:00
Koitharu
f5db5c39c3 Add deleted_at db columns 2022-05-15 19:54:50 +03:00
Koitharu
7bc945b243 Merge branch 'devel' into feature/sync 2022-05-15 17:17:12 +03:00
Koitharu
e4b29b3ff9 Cleanup strings 2022-05-15 17:11:40 +03:00
Luiz-bro
046aaa0649 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (297 of 297 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-05-15 17:04:21 +03:00
Dpper
f653c74ce8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-05-15 17:04:21 +03:00
kuragehime
0c73c55b9d Translated using Weblate (Japanese)
Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (287 of 287 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-05-15 17:04:21 +03:00
Oğuz Ersen
8dec54e96f Translated using Weblate (Turkish)
Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-05-15 17:04:21 +03:00
J. Lavoie
859ae966c8 Translated using Weblate (Finnish)
Currently translated at 99.6% (296 of 297 strings)

Translated using Weblate (French)

Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (German)

Currently translated at 100.0% (297 of 297 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (294 of 295 strings)

Translated using Weblate (French)

Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (German)

Currently translated at 100.0% (295 of 295 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (287 of 288 strings)

Translated using Weblate (French)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (German)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (French)

Currently translated at 100.0% (284 of 284 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-05-15 17:04:21 +03:00
Koitharu
5abf5d3367 Update gradle and app version 2022-05-15 16:36:08 +03:00
Koitharu
0dc4e63b7a Fix large cover 2022-05-14 20:20:04 +03:00
Koitharu
d31d302896 Sync on demand 2022-05-14 19:46:20 +03:00
Koitharu
1be8760c00 Fixes 2022-05-14 13:43:59 +03:00
Koitharu
32836d05d8 Update sync headers 2022-05-13 10:23:12 +03:00
Koitharu
1a6b4ae795 Merge branch 'devel' into feature/sync 2022-05-13 10:19:30 +03:00
Koitharu
95d7ca5264 Configure default reader mode #160 #142 2022-05-12 14:39:15 +03:00
Koitharu
317252e1dd Fix isLoading LiveData 2022-05-12 13:44:05 +03:00
Koitharu
d9044b2d03 Merge branch 'release/3.2.3' into devel 2022-05-12 13:24:32 +03:00
Koitharu
b6ae4e2b41 Fix empty chapters placeholder 2022-05-12 13:16:46 +03:00
Koitharu
fce31df121 Fix download notification on download finish 2022-05-12 12:50:06 +03:00
Koitharu
d5c1d86313 Mark nsfw notifications as secure 2022-05-12 12:42:27 +03:00
Koitharu
46df41504c Hide feed section if tracker is disabled 2022-05-12 12:27:27 +03:00
Koitharu
48e232e04e Dns over https option #161 2022-05-12 12:19:48 +03:00
Koitharu
58ff7c9235 Fix branches list 2022-05-12 10:35:12 +03:00
Koitharu
730d664b91 Tune ui 2022-05-12 10:20:43 +03:00
Koitharu
36634ecca1 Option to undo removing from favourites 2022-05-11 16:23:37 +03:00
Koitharu
10c03ff01a Update version in github templates 2022-05-11 12:28:50 +03:00
Koitharu
e85b9db118 Show stub if favourite categories empty 2022-05-11 12:20:06 +03:00
Koitharu
f6b0a7c780 Update parsers 2022-05-11 12:11:37 +03:00
Koitharu
3e785a2555 Refactor and optimization 2022-05-11 10:52:21 +03:00
Koitharu
1cbb825892 Bookmarks feature 2022-05-10 15:40:39 +03:00
Koitharu
82efa8298d Cache shikimori user 2022-05-10 10:59:19 +03:00
Koitharu
c2ba716916 Merge branch 'devel' into feature/shikimori 2022-05-09 15:31:15 +03:00
Koitharu
161bc5f69d Show stub if favourite categories empty 2022-05-09 15:27:00 +03:00
Koitharu
b17237eb6b Fix favourite categories edit 2022-05-09 13:02:45 +03:00
Koitharu
f61497ffd9 Shikimori manga tracking selection list 2022-05-09 12:57:38 +03:00
Koitharu
7f3c46942d Merge branch 'devel' into feature/shikimori 2022-05-09 09:42:21 +03:00
Koitharu
4771882f50 Edit favourite category activity 2022-05-09 09:02:58 +03:00
Koitharu
345a1379ae Merge branch 'feature/tracker_categories' into devel 2022-05-07 15:06:30 +03:00
Koitharu
33ab7f4d95 Cleanup preference_toggle_header 2022-05-07 15:06:17 +03:00
Zakhar Timoshenko
2a97cb34d7 Change root view of preference_toggle_header 2022-05-07 14:01:07 +03:00
Koitharu
03cbd8410f Remove travis.ci integration 2022-05-07 09:21:32 +03:00
Koitharu
3c54bdd003 Merge branch 'master' into devel 2022-05-07 09:17:47 +03:00
Koitharu
ba0a94e525 Merge branch 'devel' of github.com:nv95/Kotatsu into devel 2022-05-07 09:16:38 +03:00
Luiz-bro
b439e0c2c2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (281 of 281 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-05-07 09:15:17 +03:00
J. Lavoie
f9281850ad Translated using Weblate (Finnish)
Currently translated at 99.6% (280 of 281 strings)

Translated using Weblate (French)

Currently translated at 100.0% (281 of 281 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (281 of 281 strings)

Translated using Weblate (German)

Currently translated at 100.0% (281 of 281 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-05-07 09:15:17 +03:00
Dpper
4d5d25834e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (281 of 281 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Ukrainian)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-05-07 09:15:17 +03:00
kuragehime
9e706ea096 Translated using Weblate (Japanese)
Currently translated at 100.0% (281 of 281 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-05-07 09:15:17 +03:00
Oğuz Ersen
46fe2bb8ac Translated using Weblate (Turkish)
Currently translated at 100.0% (281 of 281 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-05-07 09:15:17 +03:00
Koitharu
6405523232 Fix CategoryListModel equals/hashcode 2022-05-07 08:50:33 +03:00
Zakhar Timoshenko
930819ffa2 Fix setting tracker on favourites screen 2022-05-06 20:59:43 +03:00
Zakhar Timoshenko
fa150e45ff [Issue template] Update version 2022-05-06 20:15:29 +03:00
Zakhar Timoshenko
400a2b14f7 Change a bit preference_toggle_header view 2022-05-06 16:54:41 +03:00
Zakhar Timoshenko
a40322b2e7 Fix crash on first database initialization 2022-05-06 16:53:55 +03:00
Koitharu
de9c1017b3 Update parsers 2022-05-06 15:45:20 +03:00
Koitharu
2709d40fc0 Fix BottomSheet edge-to-edge mode 2022-05-06 13:51:55 +03:00
Koitharu
45b42ad5bd Revert "Fix bottom sheet navbar color"
This reverts commit fdd4f5abca.
2022-05-06 12:57:37 +03:00
Koitharu
878df24a64 Add voice search 2022-05-06 10:52:51 +03:00
Koitharu
b759f8d0a0 Fix pages filename #151 2022-05-05 16:46:44 +03:00
Koitharu
23e7aa2aaa Fix images scale type 2022-05-05 15:57:05 +03:00
Koitharu
fdd4f5abca Fix bottom sheet navbar color 2022-05-05 15:51:30 +03:00
Koitharu
c695468aec Fix cold launch 2022-05-05 15:43:07 +03:00
Koitharu
9166716f2a Update version 2022-05-05 15:21:10 +03:00
Zakhar Timoshenko
3407e74e99 Fix FavouriteCategoriesDialog toolbar in album orientation 2022-05-05 15:16:30 +03:00
Koitharu
6969f40fa0 Merge branch 'devel' of github.com:nv95/Kotatsu into devel 2022-05-05 15:13:12 +03:00
Koitharu
11fc8b6642 Configure manga tracker for each favourite category 2022-05-05 15:11:28 +03:00
Zakhar Timoshenko
4e4024c182 Fix FavouriteCategoriesDialog toolbar in album orientation 2022-05-04 23:00:44 +03:00
Zakhar Timoshenko
1d1931f721 [Issue template] Update version 2022-05-04 19:01:32 +03:00
Koitharu
ffad6a4ae6 Upgrade coil to v2 2022-05-04 13:20:00 +03:00
Koitharu
fbc86f6d3b JWT Authorization 2022-05-03 17:47:27 +03:00
Koitharu
00e1aac984 Merge branch 'devel' into feature/sync 2022-05-03 16:28:24 +03:00
Koitharu
4c5314fe59 Update parsers 2022-05-02 14:53:31 +03:00
Koitharu
96be49aa83 Update monochrome launcher icon 2022-05-02 09:39:01 +03:00
Koitharu
28b556121b Show new sources on app startup 2022-05-02 09:35:21 +03:00
Koitharu
558c19e526 Update parsers and version 2022-05-01 09:24:45 +03:00
Luiz-bro
59c2d20311 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (280 of 280 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Luna Jernberg
fa1f2cbf51 Translated using Weblate (Swedish)
Currently translated at 98.5% (275 of 279 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
J. Lavoie
de8739f143 Translated using Weblate (Finnish)
Currently translated at 99.6% (279 of 280 strings)

Translated using Weblate (French)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (German)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (278 of 279 strings)

Translated using Weblate (French)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (German)

Currently translated at 100.0% (279 of 279 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
kuragehime
9aa28f6fd2 Translated using Weblate (Japanese)
Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (279 of 279 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Oğuz Ersen
a2b1699047 Translated using Weblate (Turkish)
Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (279 of 279 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
mondstern
2dce65a448 Translated using Weblate (German)
Currently translated at 98.9% (276 of 279 strings)

Co-authored-by: mondstern <mondstern@snopyta.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Koitharu
830cc66933 JWT authorization for sync (Draft) 2022-05-01 09:18:02 +03:00
Koitharu
8869bafe9e Compression for sync 2022-04-29 17:06:55 +03:00
Koitharu
6ea98fa056 Update sync helper 2022-04-29 16:12:15 +03:00
Koitharu
3d68d7c818 Reuse PageLoader in PagesThumbnailsSheet 2022-04-29 12:45:28 +03:00
Koitharu
4987d43042 Fix pages saving #151 2022-04-29 11:41:14 +03:00
Koitharu
684b494edb Fix concurrent manga downloading #154 2022-04-29 10:07:21 +03:00
Koitharu
714b708fa9 Fix npe on getExternalFilesDirs #158 2022-04-29 09:04:41 +03:00
Koitharu
c462c19a8b Option to hide 'All categories' tab from favourites 2022-04-28 16:46:55 +03:00
Koitharu
837fb91133 First step syncronization implementation 2022-04-28 15:02:29 +03:00
Koitharu
e34acf010e Update parsers 2022-04-23 19:31:02 +03:00
Koitharu
0fb29174c5 Fix webview useragent 2022-04-23 19:31:01 +03:00
Xtimms
ca45774cdb Merge remote-tracking branch 'origin/devel' into devel 2022-04-23 19:16:46 +03:00
Xtimms
cccc2c4fe4 Create issue template 2022-04-23 19:16:15 +03:00
Koitharu
c73af2d45f Update version 2022-04-23 15:37:16 +03:00
lowak
acf7102d07 Translated using Weblate (Swedish)
Currently translated at 100.0% (274 of 274 strings)

Co-authored-by: lowak <lowak@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
2022-04-23 15:33:11 +03:00
Koitharu
75fcd31758 Fix locking app on screen rotation 2022-04-23 15:31:52 +03:00
Koitharu
7bffb5f22d Select source domains using AutoCompleteTextView 2022-04-23 15:20:57 +03:00
Koitharu
c220bd5517 Merge branch 'feature/direct-download' into devel 2022-04-23 13:57:24 +03:00
Koitharu
7c827b45d5 Merge branch 'feature/improve-list' into devel 2022-04-23 13:55:03 +03:00
Koitharu
e91d9ee38e Fix list selection corners 2022-04-23 13:54:48 +03:00
Koitharu
b6a86a6538 Cache and reuse RemoteMangaRepository instances 2022-04-23 10:39:33 +03:00
Koitharu
16b6b6c071 Add dummy manga parser for development 2022-04-23 10:31:10 +03:00
Xtimms
695feef4a6 Improve simple manga list 2022-04-21 20:15:59 +03:00
Koitharu
6bf4e0cf89 Use PageLoader for thumbnails 2022-04-20 12:57:07 +03:00
Koitharu
44d8d0f246 Fix search keyboard #150 2022-04-20 12:17:32 +03:00
Koitharu
e617e8d6d3 Add password toggle to ProtectActivity 2022-04-20 12:06:49 +03:00
Koitharu
1f411b7530 Cleanup temporary files 2022-04-20 11:50:55 +03:00
Koitharu
d64bd9d9d3 Estimate remeaning download time 2022-04-20 11:18:33 +03:00
Koitharu
f33dc8f797 Update feed ui 2022-04-20 09:25:46 +03:00
Koitharu
e63ae12c8c Delete local chapters in a service 2022-04-19 16:04:24 +03:00
Koitharu
cbd3d439cd Support multiple branches in saved manga 2022-04-19 14:35:02 +03:00
Koitharu
83eb0d9f23 Fix isLoading live data 2022-04-19 12:53:45 +03:00
Koitharu
3c739eed8e Fix empty chapters label 2022-04-19 12:43:51 +03:00
Koitharu
d77646adf1 Fix duplicate zip entry error 2022-04-19 11:32:42 +03:00
Koitharu
5b5e6cba57 Fix download error retry 2022-04-19 11:08:55 +03:00
Koitharu
8fc9b27840 Option to slowdown downloads and configure parallelism 2022-04-19 10:28:05 +03:00
Koitharu
fa536220eb Search and parallelism in LocalMangaRepository.getList 2022-04-19 09:25:12 +03:00
Koitharu
98f16774c4 Delete whole manga if all chapters are removed 2022-04-19 08:22:54 +03:00
Koitharu
ce8f57c3ca Disable update checking if not supported #147 2022-04-18 20:11:23 +03:00
Koitharu
be66106336 Removing selected chapters from local storage 2022-04-18 20:00:43 +03:00
Koitharu
16c8641a07 Fix concurrent downloading #146 2022-04-18 16:46:35 +03:00
Koitharu
d3e9ce874a Download manga to cbz directly 2022-04-18 16:42:37 +03:00
Koitharu
aaf9c6a0bf Update parsers 2022-04-18 09:26:48 +03:00
Koitharu
c2276eb2cb Fix cover size changes in details 2022-04-17 10:24:49 +03:00
Koitharu
5fbae1256b Fix branch detection #143 2022-04-17 09:58:52 +03:00
Koitharu
d61ba80bf6 Add additional checks to download task #50 2022-04-17 09:47:21 +03:00
Koitharu
74c9fa9488 Option to save manga from history and favourites lists 2022-04-17 09:15:16 +03:00
Koitharu
ce732ccca0 Merge branch 'weblate-kotatsu-strings' into devel 2022-04-16 10:05:37 +03:00
Sergio Varela
6b99e360e0 Added translation using Weblate (Basque)
Added translation using Weblate (Basque)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
2022-04-16 09:46:16 +03:00
lowak
1c73d54a94 Translated using Weblate (Swedish)
Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Swedish)

Currently translated at 41.6% (114 of 274 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Swedish)

Added translation using Weblate (Swedish)

Co-authored-by: lowak <lowak@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Luiz-bro
36e21caf96 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (274 of 274 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Santiago José Gutiérrez Llanos
f7f9c53466 Translated using Weblate (Spanish)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (274 of 274 strings)

Co-authored-by: Santiago José Gutiérrez Llanos <gutierrezapata17@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Allan Nordhøy
3f2ee2a925 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Norwegian Bokmål)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nb_NO/
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
J. Lavoie
b1c069f62f Translated using Weblate (Italian)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (German)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (French)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Finnish)

Currently translated at 99.2% (272 of 274 strings)

Translated using Weblate (French)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (German)

Currently translated at 100.0% (274 of 274 strings)

Added translation using Weblate (Italian)

Added translation using Weblate (German)

Added translation using Weblate (French)

Translated using Weblate (Finnish)

Currently translated at 99.6% (270 of 271 strings)

Translated using Weblate (French)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (German)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (French)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (German)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (266 of 269 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Luiz-bro
22d48fce8f Added translation using Weblate (Portuguese (Brazil))
Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (268 of 269 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-04-16 09:46:16 +03:00
kuragehime
6b2666c701 Translated using Weblate (Japanese)
Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Japanese)

Translated using Weblate (Japanese)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (269 of 269 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Oğuz Ersen
414f438762 Translated using Weblate (Turkish)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (274 of 274 strings)

Added translation using Weblate (Turkish)

Translated using Weblate (Turkish)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (269 of 269 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-16 09:46:16 +03:00
Aliaksiej Razumaŭ
54f60040b5 Translated using Weblate (Belarusian)
Currently translated at 100.0% (270 of 270 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2022-04-16 09:46:16 +03:00
Koitharu
b0515033da Increase version 2022-04-16 09:40:50 +03:00
Koitharu
d37eb07301 Fix startForeground for DownloadService 2022-04-16 09:19:10 +03:00
Sergio Varela
c2fa27712c Added translation using Weblate (Basque)
Added translation using Weblate (Basque)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
2022-04-15 12:57:32 +02:00
lowak
10a2589c10 Translated using Weblate (Swedish)
Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Swedish)

Currently translated at 41.6% (114 of 274 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Swedish)

Added translation using Weblate (Swedish)

Co-authored-by: lowak <lowak@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:31 +02:00
Luiz-bro
05eb96e7c0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (274 of 274 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:31 +02:00
Santiago José Gutiérrez Llanos
15a08ad6ae Translated using Weblate (Spanish)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (274 of 274 strings)

Co-authored-by: Santiago José Gutiérrez Llanos <gutierrezapata17@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:31 +02:00
Allan Nordhøy
3e437c2ecb Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Norwegian Bokmål)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nb_NO/
Translation: Kotatsu/plurals
2022-04-15 12:57:30 +02:00
J. Lavoie
fea667b87c Translated using Weblate (Italian)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (German)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (French)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Finnish)

Currently translated at 99.2% (272 of 274 strings)

Translated using Weblate (French)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (German)

Currently translated at 100.0% (274 of 274 strings)

Added translation using Weblate (Italian)

Added translation using Weblate (German)

Added translation using Weblate (French)

Translated using Weblate (Finnish)

Currently translated at 99.6% (270 of 271 strings)

Translated using Weblate (French)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (German)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (French)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (German)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (269 of 269 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (266 of 269 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:30 +02:00
Luiz-bro
93eaaac084 Added translation using Weblate (Portuguese (Brazil))
Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (268 of 269 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-04-15 12:57:29 +02:00
kuragehime
a60df582a2 Translated using Weblate (Japanese)
Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Japanese)

Translated using Weblate (Japanese)

Currently translated at 100.0% (274 of 274 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (269 of 269 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:29 +02:00
Oğuz Ersen
aec1d4e0d6 Translated using Weblate (Turkish)
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (274 of 274 strings)

Added translation using Weblate (Turkish)

Translated using Weblate (Turkish)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (269 of 269 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-04-15 12:57:28 +02:00
Aliaksiej Razumaŭ
507f2e883c Translated using Weblate (Belarusian)
Currently translated at 100.0% (270 of 270 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2022-04-15 12:57:28 +02:00
Koitharu
5e82c75893 Cleanup extensions 2022-04-15 09:09:40 +03:00
Koitharu
9c9a389aa5 Rethrow CancellationException 2022-04-14 08:07:57 +03:00
Koitharu
1b3af70690 Add WindowInsetHolder view 2022-04-14 08:06:16 +03:00
Koitharu
2e17efe82b Update parsers 2022-04-11 18:43:52 +03:00
Koitharu
9d1c4bd660 Add more shikimori api methods 2022-04-11 13:17:26 +03:00
Koitharu
3b357eb509 Merge branch 'devel' into feature/shikimori 2022-04-11 11:06:15 +03:00
Koitharu
5bed854b9c Refactor entity mapping 2022-04-10 11:00:05 +03:00
Koitharu
7262b403f0 Hide reading fab if history is empty 2022-04-10 10:25:11 +03:00
Koitharu
a6fcbefc7b Update strings 2022-04-09 18:47:25 +03:00
Koitharu
7f9ea0efa0 Merge branch 'feature/multiselect' into devel 2022-04-09 18:39:25 +03:00
Koitharu
934861322e Migrate to ExtendedFloatingActionButton 2022-04-09 18:35:07 +03:00
Koitharu
e008fbab9b Merge branch 'devel' into feature/multiselect 2022-04-09 08:29:57 +03:00
Koitharu
2cd9ea19fd Update dependencies 2022-04-09 08:28:01 +03:00
Koitharu
699a249620 Merge branch 'documentation-update' of https://github.com/grrrrr/Kotatsu into devel 2022-04-09 07:35:16 +03:00
Koitharu
6c87d5b0bc Add check to avoid TransactionTooLargeException 2022-04-08 18:15:04 +03:00
Koitharu
c92bdae842 Add tags blacklist option for suggestions 2022-04-08 14:56:45 +03:00
Koitharu
6ca9608a80 Remove CurlLoggingInterceptor 2022-04-07 17:23:59 +03:00
Koitharu
8f9c0cbff1 Fix tags suggestion 2022-04-07 17:20:02 +03:00
Koitharu
cc6b114e4d Improve suggestions worker 2022-04-07 17:04:11 +03:00
grrrrr
3d5c2123d4 Update full_description.txt
- Remove HTML code so displaying on sites such as f-droid does not create a lot of wasted space
2022-04-06 19:13:43 +00:00
grrrrr
36b4e16b7c Update full_description.txt
- Remote HTML code so displaying on sites such as f-droid does not create a lot of wasted space
- add additional features taken from updated README.md
2022-04-06 19:12:15 +00:00
grrrrr
3ebd074e93 Update README.md
change feature "localized in" to "available in
2022-04-06 19:10:23 +00:00
grrrrr
e9b2b545a4 Update README.md
- Add additional features (password protection and localization) to list.
- Add details on how to contribute to translation
2022-04-06 19:05:33 +00:00
Koitharu
cca6d5fa04 Migrate to expedited jobs 2022-04-06 18:38:37 +03:00
Koitharu
36a7a3ebbc Fix DownloadService foreground notification #50 2022-04-06 17:24:10 +03:00
Koitharu
48ec9a1ea9 Merge branch 'feature/settings' into devel 2022-04-06 17:23:08 +03:00
Koitharu
76a9a0d1ab ActionMode selection in manga lists 2022-04-06 17:21:09 +03:00
Koitharu
f2175b40c0 Improve android AutoBackup support 2022-04-05 07:40:00 +03:00
Koitharu
85b992ca32 Remove SimpleSettingsActivity 2022-04-04 10:02:20 +03:00
Koitharu
41fb351fe0 Use master-detals pattern for settings 2022-04-04 09:41:57 +03:00
Koitharu
786914b1a6 Shikimori authorization 2022-03-10 19:54:28 +02:00
937 changed files with 34511 additions and 10300 deletions

View File

@@ -5,15 +5,16 @@ charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = false
insert_final_newline = true
max_line_length = 120
tab_width = 4
# noinspection EditorConfigKeyCorrectness
disabled_rules=no-wildcard-imports,no-unused-imports
disabled_rules = no-wildcard-imports, no-unused-imports
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
[{*.kt,*.kts}]
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL

29
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,29 @@
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app (https://github.com/KotatsuApp/Kotatsu/releases/latest)
- If this is an issue with a parser, that I should be opening an issue in https://github.com/KotatsuApp/kotatsu-parsers
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
* Kotatsu version: ?
* Android version: ?
* Device: ?
## Steps to reproduce
1. First step
2. Second step
## Issue/Request
?
## Other details
Additional details and attachments.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ⚠️ Source issue
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead

64
.github/ISSUE_TEMPLATE/report_bug.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: 🐞 Bug report
description: Report a bug in Kotatsu
labels: [bug]
body:
- type: textarea
id: summary
attributes:
label: Brief summary
description: Please describe, what went wrong
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Please provide a way to reproduce this issue. Screenshots or videos can be very helpful
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: false
- type: input
id: kotatsu-version
attributes:
label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**.
placeholder: |
Example: "3.3"
validations:
required: true
- type: input
id: android-version
attributes:
label: Android version
description: You can find this somewhere in your Android settings.
placeholder: |
Example: "12.0"
validations:
required: true
- type: input
id: device
attributes:
label: Device
description: List your device and model.
placeholder: |
Example: "LG Nexus 5X"
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true

View File

@@ -0,0 +1,24 @@
name: ⭐ Feature request
description: Suggest a new idea how to improve Kotatsu
labels: [feature request]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can Kotatsu be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true

29
.github/workflows/issue_moderator.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Issue moderator
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
auto-close-rules: |
[
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Kotatsu version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
}
]

5
.gitignore vendored
View File

@@ -6,9 +6,14 @@
/.idea/dictionaries
/.idea/modules.xml
/.idea/misc.xml
/.idea/discord.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
/.idea/render.experimental.xml
.DS_Store
/build
/captures

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
</component>
</project>

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

@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="39.689999"
inkscape:export-xdpi="39.689999"
inkscape:export-filename="/home/admin/Documents/projects/graphics/k/icon4.png"
width="512mm"
height="512mm"
viewBox="0 0 512 512.00002"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="icon4.svg">
<defs
id="defs2">
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter1266">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1256" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1258" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1260" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1262" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1264" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter1059">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1049" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1051" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1053" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1055" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1057" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter1071">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood1061" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1063" />
<feGaussianBlur
in="composite1"
stdDeviation="3"
result="blur"
id="feGaussianBlur1065" />
<feOffset
dx="6"
dy="6"
result="offset"
id="feOffset1067" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1069" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#0d47a1"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.175"
inkscape:cx="-361.03654"
inkscape:cy="630.78782"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1600"
inkscape:window-height="838"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
fit-margin-top="20"
fit-margin-left="20"
fit-margin-right="20"
fit-margin-bottom="20" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-51.12025,-104.74797)">
<g
id="g1028"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1030"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1032"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1034"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1036"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1038"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1040"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1042"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1044"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1046"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1048"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1050"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1052"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1054"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<g
id="g1056"
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
<path
id="path1128"
d="m 307.12025,310.74755 c -50.53732,0 -91.66608,44.85688 -91.66608,99.99965 0,55.14277 41.12954,99.99964 91.66608,99.99964 50.53653,0 91.66607,-44.85687 91.66607,-99.99964 0,-55.14277 -41.12875,-99.99965 -91.66607,-99.99965 z m -34.21238,78.72707 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33357 c 4.11951,2.06718 5.77966,7.06403 3.72889,11.18199 z m 58.33338,-24.99991 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52264,-0.27656 -3.72733,-0.8789 l -12.93901,-6.47811 -12.9398,6.47811 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05078,-4.11796 -0.39063,-9.11482 3.72733,-11.182 l 16.66634,-8.33356 c 2.34375,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33356 c 4.11874,2.06718 5.77889,7.06404 3.72811,11.182 z m 54.60606,13.81792 c 4.11795,2.06718 5.7781,7.06403 3.72733,11.18199 -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.11795,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 z"
style="fill:#ffffff;stroke-width:0.781247" />
<path
id="path1130"
d="m 415.36283,274.00237 c -3.48202,-6.90544 -6.92029,-13.41714 -10.20934,-19.37102 l -8.26716,-14.47964 c -6.79607,-11.51871 -12.25699,-19.90305 -14.78354,-23.66554 -0.7164,-43.32797 -19.12415,-53.79356 -21.25617,-54.86777 -3.20624,-1.5789 -7.09607,-0.97656 -9.6195,1.56249 -12.25621,12.25621 -20.23118,24.4632 -24.00695,30.89286 h -40.20141 c -3.77577,-6.42888 -11.75153,-18.63665 -24.00695,-30.89286 -2.52265,-2.53905 -6.39685,-3.14139 -9.6195,-1.56249 -2.13202,1.07421 -20.54055,11.5398 -21.25617,54.86777 -2.52655,3.76327 -7.98669,12.14605 -14.78276,23.66476 l -8.27341,14.49214 c -3.28983,5.95701 -6.73044,12.47105 -10.21403,19.3804 l -7.4445,15.32572 c -17.72572,38.05377 -34.30066,85.4286 -34.30066,129.72766 0,69.32085 58.26776,128.42064 60.75838,130.89485 0.91171,0.91172 2.03437,1.61171 3.25546,2.01796 1.07421,0.35859 26.78974,8.757 69.31928,8.757 2.21327,0 4.32967,-0.8789 5.89217,-2.4414 l 5.89216,-5.89216 h 9.76559 l 5.89217,5.89216 c 1.56249,1.5625 3.67811,2.4414 5.89216,2.4414 42.52954,0 68.24507,-8.39841 69.31929,-8.757 1.22109,-0.40703 2.34374,-1.10703 3.25546,-2.01796 2.48905,-2.47421 60.75681,-61.57322 60.75681,-130.89485 0,-44.29906 -16.57494,-91.67389 -34.30066,-129.72766 z M 348.7865,227.41426 c 4.60624,0 4.41171,7.35466 4.41171,11.96089 4.60623,0 12.25464,0.1 12.25464,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19528,0 -16.66634,-7.47028 -16.66634,-16.66634 0,-9.19606 7.47106,-16.66713 16.66634,-16.66713 z m -57.69823,30.14364 c 1.28593,-3.10858 4.32967,-5.14295 7.69841,-5.14295 h 16.66635 c 3.36952,0 6.41248,2.03437 7.69841,5.14295 1.28593,3.10858 0.56953,6.70545 -1.80703,9.082 l -8.33356,8.33356 c -1.62734,1.62734 -3.76014,2.4414 -5.89217,2.4414 -2.13202,0 -4.26404,-0.81406 -5.89216,-2.4414 l -8.33357,-8.33356 c -2.37421,-2.37655 -3.09061,-5.97342 -1.80468,-9.082 z m -25.63428,-30.14364 c 4.60623,0 4.4117,7.35466 4.4117,11.96089 4.60623,0 12.25465,0.1 12.25465,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19606,0 -16.66635,-7.47106 -16.66635,-16.66634 -7.8e-4,-9.19606 7.47029,-16.66713 16.66635,-16.66713 z m 41.66626,299.99893 c -59.7326,0 -108.33321,-52.34357 -108.33321,-116.66599 0,-64.32243 48.60061,-116.66599 108.33321,-116.66599 59.73259,0 108.3332,52.34356 108.3332,116.66599 0,64.32242 -48.60061,116.66599 -108.3332,116.66599 z"
style="fill:#ffffff;stroke-width:0.781247;filter:url(#filter1059)"
sodipodi:nodetypes="cccccccccccccccsccssccssccsccscsssssssssssccsscsscssssss" />
<g
style="fill:#ffffff"
id="g1138"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)"
inkscape:groupmode="layer" />
<g
style="fill:#ffffff"
id="g1140"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1142"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1144"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1146"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1148"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1150"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1152"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1154"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1156"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1158"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1160"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1162"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1164"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<g
style="fill:#ffffff"
id="g1166"
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
<path
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
d="m 344.3189,392.83707 c -4.60362,-2.75958 -5.36974,-9.69605 -1.45595,-13.18226 0.54459,-0.48508 5.34567,-3.07035 10.66909,-5.74503 7.5498,-3.79328 10.16725,-4.86303 11.89884,-4.86303 1.73503,0 4.42542,1.10391 12.3172,5.05396 11.72559,5.86898 12.60994,6.68326 12.60994,11.61118 0,3.40408 -0.99553,5.20819 -4.00363,7.25549 -3.08358,2.09867 -5.44113,1.68547 -13.60905,-2.38528 l -7.19926,-3.58796 -7.37198,3.59617 c -8.3911,4.09331 -10.26721,4.39753 -13.8552,2.24676 z"
id="path944" />
<path
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
d="m 285.98437,367.98056 c -3.86343,-2.35557 -5.1524,-8.06518 -2.66781,-11.81734 1.64304,-2.48125 20.719,-12.23981 23.92632,-12.23981 1.56364,0 4.61398,1.26582 12.2153,5.06905 8.53551,4.27064 10.3157,5.36752 11.30239,6.96403 1.75651,2.84207 1.95178,5.62136 0.58856,8.37633 -1.52635,3.08463 -3.36973,4.32306 -6.86644,4.61304 -2.68142,0.22236 -3.36743,-0.003 -10.22731,-3.35873 l -7.35311,-3.59707 -7.04119,3.52834 c -7.90523,3.96133 -10.62609,4.44409 -13.87671,2.46216 z"
id="path946" />
<path
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
d="m 228.11707,393.18031 c -1.0244,-0.54435 -2.42484,-1.80721 -3.11209,-2.80633 -1.05812,-1.53828 -1.2181,-2.32693 -1.04433,-5.14815 0.29039,-4.71472 1.41139,-5.70783 12.90113,-11.42937 7.71258,-3.84061 9.99443,-4.74971 11.92193,-4.74971 1.94819,0 4.22735,0.92952 12.47354,5.08716 8.66324,4.3679 10.26522,5.3693 11.33052,7.08263 3.53608,5.68714 -0.55313,12.95355 -7.28968,12.95355 -1.25225,0 -4.29453,-1.20187 -9.08226,-3.58799 l -7.19927,-3.58796 -7.37197,3.59617 c -8.07507,3.93914 -10.21699,4.34922 -13.52752,2.59 z"
id="path948" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,8 +1,16 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="FillClass" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="quality" value="0.25" />
</component>
</project>

View File

@@ -1,11 +0,0 @@
language: android
dist: trusty
android:
components:
- android-30
- build-tools-30.0.3
- platform-tools-30.0.5
- tools
before_install:
- yes | sdkmanager "platforms;android-30"
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug

View File

@@ -1,8 +1,8 @@
# Kotatsu
# Kotatsu
Kotatsu is a free and open source manga reader for Android.
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/KotatsuApp/Kotatsu) ![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
### Download
@@ -10,40 +10,49 @@ Kotatsu is a free and open source manga reader for Android.
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
Download APK from Github Releases:
Download APK directly from GitHub:
- [Latest release](https://github.com/nv95/Kotatsu/releases/latest)
- [Legacy build](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy) (with Android 4.1+ support)
- **[Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest)**
### Main Features
* Online manga catalogues
* Search manga by name and genre
* Reading history
* Search manga by name and genres
* Reading history and bookmarks
* Favourites organized by user-defined categories
* Downloading manga and reading it offline. Third-party CBZ archives also supported
* Tablet-optimized material design UI
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
* Shikimori integration (manga tracking)
* Password/fingerprint protect access to the app
* History and favourites synchronization across devices (coming soon)
### Screenshots
| ![Screenshot_20200226-210337](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| ![Screenshot_20200226-210405](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
| ![Screenshot_20200226-210337](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| ![Screenshot_20200226-210405](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
| ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
### Localization
[<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
Kotatsu is Free Software: You can use, study share and improve it at your
will. Specifically you can redistribute and/or modify it under the terms of the
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
### Disclaimer
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.
The developers of this application does not have any affiliation with the content providers available.
### DMCA disclaimer
The developers of this application does not have any affiliation with the content available in the app.
It is collecting from the sources freely available through any web browser.

View File

@@ -3,6 +3,7 @@ plugins {
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
android {
@@ -14,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
versionCode 400
versionName '3.0'
versionCode 491
versionName '4.0-a2'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -24,6 +25,12 @@ android {
arg 'room.schemaLocation', "$projectDir/schemas".toString()
}
}
// define this values in your local.properties file
buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\""
buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\""
resValue "string", "acra_login", "${localProperty('acra.login')}"
resValue "string", "acra_password", "${localProperty('acra.password')}"
}
buildTypes {
debug {
@@ -49,71 +56,97 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil.annotation.ExperimentalCoilApi',
]
}
lint {
abortOnError false
disable 'MissingTranslation', 'PrivateResource'
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = false
unitTests.includeAndroidResources true
unitTests.returnDefaultValues false
kotlinOptions {
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
}
}
}
afterEvaluate {
compileDebugKotlin {
kotlinOptions {
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:3ea7e92e64') {
implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.1'
implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.6.0-beta01'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
implementation 'com.google.android.material:material:1.7.0-beta01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
implementation 'androidx.room:room-runtime:2.4.2'
implementation 'androidx.room:room-ktx:2.4.2'
kapt 'androidx.room:room-compiler:2.4.2'
implementation 'androidx.room:room-runtime:2.4.3'
implementation 'androidx.room:room-ktx:2.4.3'
kapt 'androidx.room:room-compiler:2.4.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okio:okio:3.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.squareup.okio:okio:3.2.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'io.insert-koin:koin-android:3.1.5'
implementation 'io.coil-kt:coil-base:1.4.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.2.0'
implementation 'io.coil-kt:coil-svg:2.1.0'
// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2'
implementation 'com.github.solkin:disk-lru-cache:1.4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
implementation 'ch.acra:acra-http:5.9.5'
implementation 'ch.acra:acra-dialog:5.9.5'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
testImplementation 'org.json:json:20220320'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'androidx.room:room-testing:2.4.2'
}
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42'
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

10
app/sampledata/genres Normal file
View File

@@ -0,0 +1,10 @@
Slice of Life, Mystery
Slice of Life, Mystery
Psychological, Romance, Comedy, Slice of Life, Supernatural
Sci-Fi, Comedy
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Adventure, Slice of Life, Mystery
Adventure, Slice of Life, Mystery

10
app/sampledata/titles Normal file
View File

@@ -0,0 +1,10 @@
Forget-me-not Vol. 1
Forget-me-not Vol. 2
La Pomme Prisoinniere
Momo Kanchou no Himitsu Kichi
Omoide Emanon
Sasurai Emanon Vol. 1
Sasurai Emanon Vol. 2
Sasurai Emanon Vol. 3
Wandering Island Vol. 1
Wandering Island Vol. 2

View File

@@ -0,0 +1,9 @@
{
"id": 4,
"title": "Read later",
"sortKey": 1,
"order": "NEWEST",
"createdAt": 1335906000000,
"isTrackingEnabled": true,
"isVisibleInLibrary": true
}

Binary file not shown.

View File

@@ -0,0 +1,163 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"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",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 1552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,36 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"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

@@ -0,0 +1,136 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"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",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,163 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"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",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541665,
"name": "2 - 1",
"number": 6,
"url": "/stranstviia_emanon/vol2/1",
"scanlator": "Sup!",
"uploadDate": 1415570400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,35 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": null,
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,154 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"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",
"number": 1,
"url": "/stranstviia_emanon/vol1/1",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540705,
"name": "1 - 2",
"number": 2,
"url": "/stranstviia_emanon/vol1/2",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540706,
"name": "1 - 3",
"number": 3,
"url": "/stranstviia_emanon/vol1/3",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540707,
"name": "1 - 4",
"number": 4,
"url": "/stranstviia_emanon/vol1/4",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433540708,
"name": "1 - 5",
"number": 5,
"url": "/stranstviia_emanon/vol1/5",
"scanlator": "Sad-Robot",
"uploadDate": 1342731600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541666,
"name": "2 - 2",
"number": 7,
"url": "/stranstviia_emanon/vol2/2",
"scanlator": "Sup!",
"uploadDate": 1419976800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541667,
"name": "2 - 3",
"number": 8,
"url": "/stranstviia_emanon/vol2/3",
"scanlator": "Sup!",
"uploadDate": 1427922000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541668,
"name": "2 - 4",
"number": 9,
"url": "/stranstviia_emanon/vol2/4",
"scanlator": "Sup!",
"uploadDate": 1436907600000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541669,
"name": "2 - 5",
"number": 10,
"url": "/stranstviia_emanon/vol2/5",
"scanlator": "Sup!",
"uploadDate": 1446674400000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433541670,
"name": "2 - 6",
"number": 11,
"url": "/stranstviia_emanon/vol2/6",
"scanlator": "Sup!",
"uploadDate": 1451512800000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542626,
"name": "3 - 1",
"number": 12,
"url": "/stranstviia_emanon/vol3/1",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542627,
"name": "3 - 2",
"number": 13,
"url": "/stranstviia_emanon/vol3/2",
"scanlator": "Sup!",
"uploadDate": 1461618000000,
"source": "READMANGA_RU"
},
{
"id": 3552943969433542628,
"name": "3 - 3",
"number": 14,
"url": "/stranstviia_emanon/vol3/3",
"scanlator": "",
"uploadDate": 1465851600000,
"source": "READMANGA_RU"
}
],
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu
import android.app.Instrumentation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
waitForIdle { cont.resume(Unit) }
}

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu
import androidx.test.platform.app.InstrumentationRegistry
import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.buffer
import okio.source
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
import kotlin.reflect.KClass
object SampleData {
private val moshi = Moshi.Builder()
.add(DateAdapter())
.add(KotlinJsonAdapterFactory())
.build()
val manga: Manga = loadAsset("manga/header.json", Manga::class)
val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class)
val tag = mangaDetails.tags.elementAt(2)
val chapter = checkNotNull(mangaDetails.chapters)[2]
val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class)
fun <T : Any> loadAsset(name: String, cls: KClass<T>): T {
val assets = InstrumentationRegistry.getInstrumentation().context.assets
return assets.open(name).use {
moshi.adapter(cls.java).fromJson(it.source().buffer())
} ?: throw RuntimeException("Cannot read asset from json \"$name\"")
}
private class DateAdapter : JsonAdapter<Date>() {
@FromJson
override fun fromJson(reader: JsonReader): Date? {
val ms = reader.nextLong()
return if (ms == 0L) {
null
} else {
Date(ms)
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: Date?) {
writer.value(value?.time ?: 0L)
}
}
}

View File

@@ -1,14 +1,12 @@
package org.koitharu.kotatsu.core.db
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koitharu.kotatsu.core.db.migrations.*
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MangaDatabaseTest {
@@ -16,40 +14,44 @@ class MangaDatabaseTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
MangaDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
MangaDatabase::class.java,
)
private val migrations = databaseMigrations
@Test
@Throws(IOException::class)
fun migrateAll() {
helper.createDatabase(TEST_DB, 1).apply {
// TODO execSQL("")
close()
fun versions() {
assertEquals(1, migrations.first().startVersion)
repeat(migrations.size) { i ->
assertEquals(i + 1, migrations[i].startVersion)
assertEquals(i + 2, migrations[i].endVersion)
}
assertEquals(DATABASE_VERSION, migrations.last().endVersion)
}
@Test
fun migrateAll() {
helper.createDatabase(TEST_DB, 1).close()
for (migration in migrations) {
helper.runMigrationsAndValidate(
TEST_DB,
migration.endVersion,
true,
migration
)
migration,
).close()
}
}
@Test
fun prePopulate() {
val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources
helper.createDatabase(TEST_DB, DATABASE_VERSION).use {
DatabasePrePopulateCallback(resources).onCreate(it)
}
}
private companion object {
const val TEST_DB = "test-db"
val migrations = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
)
}
}
}

View File

@@ -0,0 +1,77 @@
package org.koitharu.kotatsu.core.os
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.os.Build
import androidx.core.content.getSystemService
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.awaitForIdle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.history.domain.HistoryRepository
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ShortcutsUpdaterTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var historyRepository: HistoryRepository
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
@Inject
lateinit var database: MangaDatabase
@Before
fun setUp() {
hiltRule.inject()
database.clearAllTables()
}
@Test
fun testUpdateShortcuts() = runTest {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return@runTest
}
awaitUpdate()
assertTrue(getShortcuts().isEmpty())
historyRepository.addOrUpdate(
manga = SampleData.manga,
chapterId = SampleData.chapter.id,
page = 4,
scroll = 2,
percent = 0.3f,
)
awaitUpdate()
val shortcuts = getShortcuts()
assertEquals(1, shortcuts.size)
}
private fun getShortcuts(): List<ShortcutInfo> {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val manager = checkNotNull(context.getSystemService<ShortcutManager>())
return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" }
}
private suspend fun awaitUpdate() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.awaitForIdle()
shortcutsUpdater.await()
}
}

View File

@@ -0,0 +1,108 @@
package org.koitharu.kotatsu.settings.backup
import android.content.res.AssetManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
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.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class AppBackupAgentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var historyRepository: HistoryRepository
@Inject
lateinit var favouritesRepository: FavouritesRepository
@Inject
lateinit var backupRepository: BackupRepository
@Inject
lateinit var database: MangaDatabase
@Before
fun setUp() {
hiltRule.inject()
database.clearAllTables()
}
@Test
fun backupAndRestore() = runTest {
val category = favouritesRepository.createCategory(
title = SampleData.favouriteCategory.title,
sortOrder = SampleData.favouriteCategory.order,
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
)
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
historyRepository.addOrUpdate(
manga = SampleData.mangaDetails,
chapterId = SampleData.mangaDetails.chapters!![2].id,
page = 3,
scroll = 40,
percent = 0.2f,
)
val history = checkNotNull(historyRepository.getOne(SampleData.manga))
val agent = AppBackupAgent()
val backup = agent.createBackupFile(
context = InstrumentationRegistry.getInstrumentation().targetContext,
repository = backupRepository,
)
database.clearAllTables()
assertTrue(favouritesRepository.getAllManga().isEmpty())
assertNull(historyRepository.getLastOrNull())
backup.inputStream().use {
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
}
assertEquals(category, favouritesRepository.getCategory(category.id))
assertEquals(history, historyRepository.getOne(SampleData.manga))
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
assertTrue(SampleData.tag in allTags)
}
@Test
fun restoreOldBackup() {
val agent = AppBackupAgent()
val backup = File.createTempFile("backup_", ".tmp")
InstrumentationRegistry.getInstrumentation().context.assets
.open("kotatsu_test.bak", AssetManager.ACCESS_STREAMING)
.use { input ->
backup.outputStream().use { output ->
input.copyTo(output)
}
}
backup.inputStream().use {
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
}
runTest {
assertEquals(6, historyRepository.observeAll().first().size)
assertEquals(2, favouritesRepository.observeCategories().first().size)
assertEquals(15, favouritesRepository.getAllManga().size)
}
}
}

View File

@@ -0,0 +1,198 @@
package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import junit.framework.TestCase.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.Manga
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class TrackerTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var repository: TrackingRepository
@Inject
lateinit var dataRepository: MangaDataRepository
@Inject
lateinit var tracker: Tracker
@Before
fun setUp() {
hiltRule.inject()
}
@Test
fun noUpdates() = runTest {
val manga = loadManga("full.json")
tracker.deleteTrack(manga.id)
tracker.checkUpdates(manga, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(manga.id))
tracker.checkUpdates(manga, commit = true).apply {
assertTrue(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(manga.id))
}
@Test
fun hasUpdates() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaFull = loadManga("full.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assert(newChapters.isEmpty())
}
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun badIds() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaBad = loadManga("bad_ids.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaBad, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun badIds2() = runTest {
val mangaFirst = loadManga("first_chapters.json")
val mangaBad = loadManga("bad_ids.json")
val mangaFull = loadManga("full.json")
tracker.deleteTrack(mangaFirst.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaBad, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
}
@Test
fun fullReset() = runTest {
val mangaFull = loadManga("full.json")
val mangaFirst = loadManga("first_chapters.json")
val mangaEmpty = loadManga("empty.json")
tracker.deleteTrack(mangaFull.id)
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaEmpty, commit = true).apply {
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
tracker.checkUpdates(mangaEmpty, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
}
@Test
fun syncWithHistory() = runTest {
val mangaFull = loadManga("full.json")
val mangaFirst = loadManga("first_chapters.json")
tracker.deleteTrack(mangaFull.id)
tracker.checkUpdates(mangaFirst, commit = true).apply {
assertFalse(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assertEquals(3, newChapters.size)
}
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
repository.syncWithHistory(mangaFull, chapter.id)
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
repository.syncWithHistory(mangaFull, chapter.id)
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
tracker.checkUpdates(mangaFull, commit = true).apply {
assertTrue(isValid)
assert(newChapters.isEmpty())
}
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
}
private suspend fun loadManga(name: String): Manga {
val manga = SampleData.loadAsset("manga/$name", Manga::class)
dataRepository.storeManga(manga)
return manga
}
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.core.parser
import java.util.*
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
/**
* This parser is just for parser development, it should not be used in releases
*/
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("", null)
override val sortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga {
TODO("Not yet implemented")
}
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
TODO("Not yet implemented")
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
TODO("Not yet implemented")
}
override suspend fun getTags(): Set<MangaTag> {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.newParser
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
return if (source == MangaSource.DUMMY) {
DummyParser(loaderContext)
} else {
source.newParser(loaderContext)
}
}

View File

@@ -0,0 +1,3 @@
package org.koitharu.kotatsu.utils.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

View File

@@ -8,23 +8,31 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Kotatsu"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
<activity
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
android:exported="true">
@@ -53,18 +61,32 @@
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" />
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
<activity
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:label="@string/search_manga" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" />
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
android:label="@string/history" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
android:label="@string/favourites" />
<activity
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
android:label="@string/bookmarks" />
<activity
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
android:label="@string/suggestions" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kotatsu" />
</intent-filter>
</activity>
<activity
@@ -76,24 +98,20 @@
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
android:label="@string/favourites_categories"
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
android:label="@string/favourites"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:exported="true"
android:label="@string/manga_shelf">
android:label="@string/manga_shelf"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
android:label="@string/search" />
<activity
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
@@ -104,18 +122,64 @@
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
android:label="@string/downloads" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
android:label="@string/downloads"
android:launchMode="singleTop"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
<activity
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
<activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:label="@string/sync" />
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
android:stopWithTask="false"
android:foregroundServiceType="dataSync" />
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service android:name="org.koitharu.kotatsu.local.ui.ImportService" />
<service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthenticatorService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator_sync" />
</service>
<service
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
android:exported="false"
android:label="@string/favourites"
android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_favourites" />
</service>
<service
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
android:exported="false"
android:label="@string/history"
android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_history" />
</service>
<provider
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
@@ -130,6 +194,22 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<provider
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider"
android:authorities="org.koitharu.kotatsu.favourites"
android:exported="false"
android:label="@string/favourites"
android:syncable="true" />
<provider
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider"
android:authorities="org.koitharu.kotatsu.history"
android:exported="false"
android:label="@string/history"
android:syncable="true" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
@@ -163,4 +243,4 @@
</application>
</manifest>
</manifest>

View File

@@ -1,73 +1,112 @@
package org.koitharu.kotatsu
import android.app.Application
import android.content.Context
import android.os.StrictMode
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.acra.ReportField
import org.acra.config.dialog
import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.AppCrashHandler
import org.koitharu.kotatsu.core.ui.uiModule
import org.koitharu.kotatsu.details.detailsModule
import org.koitharu.kotatsu.favourites.favouritesModule
import org.koitharu.kotatsu.history.historyModule
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
class KotatsuApp : Application() {
@HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider {
@Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
@Inject
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
@Inject
lateinit var database: MangaDatabase
@Inject
lateinit var settings: AppSettings
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
enableStrictMode()
}
initKoin()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
val widgetUpdater = WidgetUpdater(applicationContext)
widgetUpdater.subscribeToFavourites(get())
widgetUpdater.subscribeToHistory(get())
AppCompatDelegate.setDefaultNightMode(settings.theme)
setupActivityLifecycleCallbacks()
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
}
}
private fun initKoin() {
startKoin {
androidContext(this@KotatsuApp)
modules(
networkModule,
databaseModule,
githubModule,
uiModule,
mainModule,
searchModule,
localModule,
favouritesModule,
historyModule,
remoteListModule,
detailsModule,
trackerModule,
settingsModule,
readerModule,
appWidgetModule,
suggestionsModule,
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
excludeMatchingSharedPreferencesKeys = listOf(
"sources_\\w+",
)
httpSender {
uri = getString(R.string.url_error_report)
basicAuthLogin = getString(R.string.acra_login)
basicAuthPassword = getString(R.string.acra_password)
httpMethod = HttpSender.Method.POST
}
reportContent = listOf(
ReportField.PACKAGE_NAME,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.ANDROID_VERSION,
ReportField.PHONE_MODEL,
ReportField.STACK_TRACE,
ReportField.CRASH_CONFIGURATION,
ReportField.SHARED_PREFERENCES,
)
dialog {
text = getString(R.string.crash_text)
title = getString(R.string.error_occurred)
positiveButtonText = getString(R.string.send)
resIcon = R.drawable.ic_alert_outline
resTheme = android.R.style.Theme_Material_Light_Dialog_Alert
}
}
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
@WorkerThread
private fun setupDatabaseObservers() {
val tracker = database.invalidationTracker
databaseObservers.forEach {
tracker.addObserver(it)
}
}
private fun setupActivityLifecycleCallbacks() {
activityLifecycleCallbacks.forEach {
registerActivityLifecycleCallbacks(it)
}
}
@@ -76,7 +115,7 @@ class KotatsuApp : Application() {
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
@@ -85,7 +124,7 @@ class KotatsuApp : Application() {
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
@@ -96,4 +135,4 @@ class KotatsuApp : Application() {
.detectFragmentTagUsage()
.build()
}
}
}

View File

@@ -1,28 +1,46 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.room.withTransaction
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.parsers.util.await
class MangaDataRepository(private val db: MangaDatabase) {
private const val MIN_WEBTOON_RATIO = 2
class MangaDataRepository @Inject constructor(
private val okHttpClient: OkHttpClient,
private val db: MangaDatabase,
) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.mangaDao.upsert(manga.toEntity(), tags)
db.preferencesDao.upsert(
MangaPrefsEntity(
mangaId = manga.id,
mode = mode.id
)
mode = mode.id,
),
)
}
}
@@ -37,21 +55,74 @@ class MangaDataRepository(private val db: MangaDatabase) {
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != 0L -> db.mangaDao.find(intent.mangaId)?.toManga()
intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri
}
suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.mangaDao.upsert(manga.toEntity(), tags)
}
}
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).mapToSet {
it.toMangaTag()
return db.tagsDao.findTags(source.name).toMangaTags()
}
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
okHttpClient.newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
companion object {
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}
}

View File

@@ -1,72 +0,0 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream
import java.util.zip.ZipFile
object MangaUtils : KoinComponent {
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
get<OkHttpClient>().newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * 2 < size.height
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
return null
}
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.base.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
fun interface ReversibleHandle {
suspend fun reverse()
}
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
runCatching {
reverse()
}.onFailure {
it.printStackTraceDebug()
}
}
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
this.reverse()
other.reverse()
}

View File

@@ -21,14 +21,14 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
viewBinding = binding
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root)
.also(::onBuildDialog)
.run(::onBuildDialog)
.create()
}
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
) = viewBinding?.root
@CallSuper
@@ -37,9 +37,9 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
super.onDestroyView()
}
open fun onBuildDialog(builder: MaterialAlertDialogBuilder) = Unit
open fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder = builder
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}
}

View File

@@ -7,26 +7,38 @@ import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import org.koin.android.ext.android.get
import dagger.hilt.android.EntryPointAccessors
import javax.inject.Inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.base.ui.util.BaseActivityEntryPoint
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.base.ui.util.inject
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.getThemeColor
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(),
WindowInsetsDelegate.WindowInsetsListener {
@Inject
lateinit var settings: AppSettings
protected lateinit var binding: B
private set
@@ -36,11 +48,17 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
val actionModeDelegate = ActionModeDelegate()
override fun onCreate(savedInstanceState: Bundle?) {
val settings = get<AppSettings>()
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
val isAmoled = settings.isAmoledTheme
val isDynamic = settings.isDynamicTheme
// TODO support DialogWhenLarge theme
when {
settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -87,17 +105,33 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
protected fun isDarkAmoledTheme(): Boolean {
val uiMode = resources.configuration.uiMode
val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
return isNight && get<AppSettings>().isAmoledTheme
return isNight && settings.isAmoledTheme
}
@CallSuper
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
actionModeDelegate.onSupportActionModeStarted(mode)
val actionModeColor = ColorUtils.compositeColors(
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
getThemeColor(com.google.android.material.R.attr.colorSurface),
)
val insets = ViewCompat.getRootWindowInsets(binding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
view?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
window.statusBarColor = actionModeColor
}
@CallSuper
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
actionModeDelegate.onSupportActionModeFinished(mode)
window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
}
override fun onBackPressed() {

View File

@@ -2,11 +2,11 @@ package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.R as materialR
@@ -14,6 +14,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
import org.koitharu.kotatsu.utils.ext.displayCompat
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
@@ -28,10 +30,24 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View {
val binding = onInflateView(inflater, container)
viewBinding = binding
// Enforce max width for tablets
val width = resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
if (width > 0) {
behavior?.maxWidth = width
}
// Set peek height to 50% display height
requireContext().displayCompat?.let {
val metrics = DisplayMetrics()
it.getRealMetrics(metrics)
behavior?.peekHeight = (metrics.heightPixels * 0.4).toInt()
}
return binding.root
}
@@ -41,9 +57,16 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
} else super.onCreateDialog(savedInstanceState)
return AppBottomSheetDialog(requireContext(), theme)
}
fun addBottomSheetCallback(callback: BottomSheetBehavior.BottomSheetCallback) {
val b = behavior ?: return
b.addBottomSheetCallback(callback)
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
if (rootView != null) {
callback.onStateChanged(rootView, b.state)
}
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
@@ -60,4 +83,4 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
}
b.isDraggable = !isLocked
}
}
}

View File

@@ -6,10 +6,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
abstract class BaseFragment<B : ViewBinding> : Fragment(),
abstract class BaseFragment<B : ViewBinding> :
Fragment(),
WindowInsetsDelegate.WindowInsetsListener {
private var viewBinding: B? = null
@@ -23,6 +25,9 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(),
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@@ -7,10 +7,12 @@ import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
@@ -18,7 +20,8 @@ private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
abstract class BaseFullscreenActivity<B : ViewBinding> :
BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
showSystemUI()
}
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
@Suppress("DEPRECATION")
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
@Suppress("DEPRECATION")
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}

View File

@@ -8,16 +8,21 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
@AndroidEntryPoint
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
@Inject
lateinit var settings: AppSettings
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
@@ -39,16 +44,20 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : Pre
override fun onResume() {
super.onResume()
if (titleId != 0) {
activity?.setTitle(titleId)
setTitle(getString(titleId))
}
}
@CallSuper
override fun onWindowInsetsChanged(insets: Insets) {
listView.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom
bottom = insets.bottom,
)
}
@Suppress("UsePropertyAccessSyntax")
protected fun setTitle(title: CharSequence) {
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title)
}
}

View File

@@ -1,18 +1,25 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class BaseViewModel : ViewModel() {
val onError = SingleLiveEvent<Throwable>()
val isLoading = MutableLiveData(false)
protected val loadingCounter = CountedBooleanLiveData()
protected val errorEvent = SingleLiveEvent<Throwable>()
val onError: LiveData<Throwable>
get() = errorEvent
val isLoading: LiveData<Boolean>
get() = loadingCounter
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
@@ -25,20 +32,18 @@ abstract class BaseViewModel : ViewModel() {
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
isLoading.postValue(true)
loadingCounter.increment()
try {
block()
} finally {
isLoading.postValue(false)
loadingCounter.decrement()
}
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) {
throwable.printStackTrace()
}
throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
onError.postCall(throwable)
errorEvent.postCall(throwable)
}
}
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.base.ui
import android.app.Service
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
abstract class CoroutineIntentService : BaseService() {
private val mutex = Mutex()
protected open val dispatcher: CoroutineDispatcher = Dispatchers.Default
final override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
launchCoroutine(intent, startId)
return Service.START_REDELIVER_INTENT
}
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
mutex.withLock {
try {
withContext(dispatcher) {
processIntent(intent)
}
} finally {
stopSelf(startId)
}
}
}
protected abstract suspend fun processIntent(intent: Intent?)
}

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.graphics.Color
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialog
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
/**
* https://github.com/material-components/material-components-android/issues/2582
*/
@Suppress("DEPRECATION")
override fun onAttachedToWindow() {
val window = window
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
super.onAttachedToWindow()
if (window != null) {
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
if (drawEdgeToEdge) {
// Copied from super.onAttachedToWindow:
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Fix super-class's window flag bug by respecting the initial system UI visibility:
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
}
}
}
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.DialogInterface
class RememberSelectionDialogListener(initialValue: Int) : DialogInterface.OnClickListener {
var selection: Int = initialValue
private set
override fun onClick(dialog: DialogInterface?, which: Int) {
selection = which
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
@@ -12,7 +13,6 @@ import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.utils.ext.inflate
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
@@ -66,7 +66,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
val volumes = getAvailableVolumes(storageManager)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: parent.inflate(R.layout.item_storage)
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
view.tag = it
}

View File

@@ -1,89 +0,0 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor(
private val delegate: AlertDialog,
) : DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context) {
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setHint(@StringRes hintResId: Int): Builder {
binding.inputEdit.hint = binding.root.context.getString(hintResId)
return this
}
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
with(binding.inputLayout) {
counterMaxLength = maxLength
isCounterEnabled = maxLength > 0
}
if (strict && maxLength > 0) {
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
}
return this
}
fun setInputType(inputType: Int): Builder {
binding.inputEdit.inputType = inputType
return this
}
fun setText(text: String): Builder {
binding.inputEdit.setText(text)
binding.inputEdit.setSelection(text.length)
return this
}
fun setPositiveButton(
@StringRes textId: Int,
listener: (DialogInterface, String) -> Unit
): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, binding.inputEdit.text?.toString().orEmpty())
}
return this
}
fun setNegativeButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener? = null
): Builder {
delegate.setNegativeButton(textId, listener)
return this
}
fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder {
delegate.setOnCancelListener(listener)
return this
}
fun create() =
TextInputDialog(delegate.create())
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
class AdapterDelegateClickListenerAdapter<I>(
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<I, *>,
private val clickListener: OnListItemClickListener<I>,
) : OnClickListener, OnLongClickListener {
override fun onClick(v: View) {
clickListener.onItemClick(adapterDelegate.item, v)
}
override fun onLongClick(v: View): Boolean {
return clickListener.onItemLongClick(adapterDelegate.item, v)
}
}

View File

@@ -0,0 +1,203 @@
package org.koitharu.kotatsu.base.ui.list
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
private const val KEY_SELECTION = "selection"
private const val PROVIDER_NAME = "selection_decoration"
class ListSelectionController(
private val activity: Activity,
private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
val count: Int
get() = decoration.checkedItemsCount
init {
registryOwner.lifecycle.addObserver(StateEventObserver())
}
fun snapshot(): Set<Long> {
return peekCheckedIds().toSet()
}
fun peekCheckedIds(): Set<Long> {
return decoration.checkedItemsIds
}
fun clear() {
decoration.clearSelection()
notifySelectionChanged()
}
fun addAll(ids: Collection<Long>) {
if (ids.isEmpty()) {
return
}
decoration.checkAll(ids)
notifySelectionChanged()
}
fun attachToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addItemDecoration(decoration)
}
override fun saveState(): Bundle {
val bundle = Bundle(1)
bundle.putLongArray(KEY_SELECTION, peekCheckedIds().toLongArray())
return bundle
}
fun onItemClick(id: Long): Boolean {
if (decoration.checkedItemsCount != 0) {
decoration.toggleItemChecked(id)
if (decoration.checkedItemsCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
notifySelectionChanged()
return true
}
return false
}
fun onItemLongClick(id: Long): Boolean {
startActionMode()
return actionMode?.also {
decoration.setItemIsChecked(id, true)
notifySelectionChanged()
} != null
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onCreateActionMode(this, mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onPrepareActionMode(this, mode, menu)
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return callback.onActionItemClicked(this, mode, item)
}
override fun onDestroyActionMode(mode: ActionMode) {
callback.onDestroyActionMode(this, mode)
clear()
actionMode = null
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
}
private fun notifySelectionChanged() {
val count = decoration.checkedItemsCount
callback.onSelectionChanged(this, count)
if (count == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
private fun restoreState(ids: Collection<Long>) {
if (ids.isEmpty() || decoration.checkedItemsCount != 0) {
return
}
decoration.checkAll(ids)
startActionMode()
notifySelectionChanged()
}
@Deprecated("")
interface Callback : Callback2 {
fun onSelectionChanged(count: Int)
fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
fun onDestroyActionMode(mode: ActionMode) = Unit
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
onSelectionChanged(count)
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(
controller: ListSelectionController,
mode: ActionMode,
item: MenuItem,
): Boolean = onActionItemClicked(mode, item)
override fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) {
onDestroyActionMode(mode)
}
}
interface Callback2 {
fun onSelectionChanged(controller: ListSelectionController, count: Int)
fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.title = controller.count.toString()
return true
}
fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean
fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) = Unit
}
private inner class StateEventObserver : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
registry.registerSavedStateProvider(PROVIDER_NAME, this@ListSelectionController)
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
if (state != null) {
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
restoreState(state.getLongArray(KEY_SELECTION)?.toList().orEmpty())
}
}
}
}
}
}
}

View File

@@ -0,0 +1,225 @@
package org.koitharu.kotatsu.base.ui.list
import android.app.Activity
import android.os.Bundle
import android.util.ArrayMap
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
private const val PROVIDER_NAME = "selection_decoration_sectioned"
class SectionedSelectionController<T : Any>(
private val activity: Activity,
private val owner: SavedStateRegistryOwner,
private val callback: Callback<T>,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
private var pendingData: MutableMap<String, Collection<Long>>? = null
private val decorations = ArrayMap<T, AbstractSelectionItemDecoration>()
val count: Int
get() = decorations.values.sumOf { it.checkedItemsCount }
init {
owner.lifecycle.addObserver(StateEventObserver())
}
fun snapshot(): Map<T, Set<Long>> {
return decorations.mapValues { it.value.checkedItemsIds.toSet() }
}
fun peekCheckedIds(): Map<T, Set<Long>> {
return decorations.mapValues { it.value.checkedItemsIds }
}
fun clear() {
decorations.values.forEach {
it.clearSelection()
}
notifySelectionChanged()
}
fun attachToRecyclerView(section: T, recyclerView: RecyclerView) {
val decoration = getDecoration(section)
val pendingIds = pendingData?.remove(section.toString())
if (!pendingIds.isNullOrEmpty()) {
decoration.checkAll(pendingIds)
startActionMode()
notifySelectionChanged()
}
recyclerView.addItemDecoration(decoration)
if (pendingData?.isEmpty() == true) {
pendingData = null
}
}
override fun saveState(): Bundle {
val bundle = Bundle(decorations.size)
for ((k, v) in decorations) {
bundle.putLongArray(k.toString(), v.checkedItemsIds.toLongArray())
}
return bundle
}
fun onItemClick(section: T, id: Long): Boolean {
val decoration = getDecoration(section)
if (isInSelectionMode()) {
decoration.toggleItemChecked(id)
if (isInSelectionMode()) {
actionMode?.invalidate()
} else {
actionMode?.finish()
}
notifySelectionChanged()
return true
}
return false
}
fun onItemLongClick(section: T, id: Long): Boolean {
val decoration = getDecoration(section)
startActionMode()
return actionMode?.also {
decoration.setItemIsChecked(id, true)
notifySelectionChanged()
} != null
}
fun getSectionCount(section: T): Int {
return decorations[section]?.checkedItemsCount ?: 0
}
fun addToSelection(section: T, ids: Collection<Long>): Boolean {
val decoration = getDecoration(section)
startActionMode()
return actionMode?.also {
decoration.checkAll(ids)
notifySelectionChanged()
} != null
}
fun clearSelection(section: T) {
decorations[section]?.clearSelection() ?: return
notifySelectionChanged()
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onCreateActionMode(this, mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onPrepareActionMode(this, mode, menu)
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return callback.onActionItemClicked(this, mode, item)
}
override fun onDestroyActionMode(mode: ActionMode) {
callback.onDestroyActionMode(this, mode)
clear()
actionMode = null
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
}
}
private fun isInSelectionMode(): Boolean {
return decorations.values.any { x -> x.checkedItemsCount > 0 }
}
private fun notifySelectionChanged() {
val count = this.count
callback.onSelectionChanged(this, count)
if (count == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
private fun restoreState(ids: MutableMap<String, Collection<Long>>) {
if (ids.isEmpty() || isInSelectionMode()) {
return
}
for ((k, v) in decorations) {
val items = ids.remove(k.toString())
if (!items.isNullOrEmpty()) {
v.checkAll(items)
}
}
pendingData = ids
if (isInSelectionMode()) {
startActionMode()
notifySelectionChanged()
}
}
private fun getDecoration(section: T): AbstractSelectionItemDecoration {
return decorations.getOrPut(section) {
callback.onCreateItemDecoration(this, section)
}
}
interface Callback<T : Any> {
fun onSelectionChanged(controller: SectionedSelectionController<T>, count: Int)
fun onCreateActionMode(controller: SectionedSelectionController<T>, mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(controller: SectionedSelectionController<T>, mode: ActionMode, menu: Menu): Boolean {
mode.title = controller.count.toString()
return true
}
fun onDestroyActionMode(controller: SectionedSelectionController<T>, mode: ActionMode) = Unit
fun onActionItemClicked(
controller: SectionedSelectionController<T>,
mode: ActionMode,
item: MenuItem,
): Boolean
fun onCreateItemDecoration(
controller: SectionedSelectionController<T>,
section: T,
): AbstractSelectionItemDecoration
}
private inner class StateEventObserver : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_CREATE) {
val registry = owner.savedStateRegistry
registry.registerSavedStateProvider(PROVIDER_NAME, this@SectionedSelectionController)
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
if (state != null) {
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
restoreState(
state.keySet()
.associateWithTo(HashMap()) { state.getLongArray(it)?.toList().orEmpty() },
)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,111 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val boundsF = RectF()
protected val selection = HashSet<Long>()
protected var hasBackground: Boolean = true
protected var hasForeground: Boolean = false
protected var isIncludeDecorAndMargins: Boolean = true
val checkedItemsCount: Int
get() = selection.size
val checkedItemsIds: Set<Long>
get() = selection
fun toggleItemChecked(id: Long) {
if (!selection.remove(id)) {
selection.add(id)
}
}
fun setItemIsChecked(id: Long, isChecked: Boolean) {
if (isChecked) {
selection.add(id)
} else {
selection.remove(id)
}
}
fun checkAll(ids: Collection<Long>) {
selection.addAll(ids)
}
fun clearSelection() {
selection.clear()
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (hasBackground) {
doDraw(canvas, parent, state, false)
} else {
super.onDraw(canvas, parent, state)
}
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (hasForeground) {
doDraw(canvas, parent, state, true)
} else {
super.onDrawOver(canvas, parent, state)
}
}
private fun doDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State, isOver: Boolean) {
val checkpoint = canvas.save()
if (parent.clipToPadding) {
canvas.clipRect(
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
parent.height - parent.paddingBottom
)
}
for (child in parent.children) {
val itemId = getItemId(parent, child)
if (itemId != NO_ID && itemId in selection) {
if (isIncludeDecorAndMargins) {
parent.getDecoratedBoundsWithMargins(child, bounds)
} else {
bounds.set(child.left, child.top, child.right, child.bottom)
}
boundsF.set(bounds)
boundsF.offset(child.translationX, child.translationY)
if (isOver) {
onDrawForeground(canvas, parent, child, boundsF, state)
} else {
onDrawBackground(canvas, parent, child, boundsF, state)
}
}
}
canvas.restoreToCount(checkpoint)
}
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
protected open fun onDrawBackground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) = Unit
protected open fun onDrawForeground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) = Unit
}

View File

@@ -11,8 +11,8 @@ class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDec
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
state: RecyclerView.State,
) {
outRect.set(spacing, spacing, spacing, spacing)
}
}
}

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.util.SparseIntArray
import android.view.View
import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.recyclerview.widget.RecyclerView
class TypedSpacingItemDecoration(
vararg spacingMapping: Pair<Int, Int>,
private val fallbackSpacing: Int = 0,
) : RecyclerView.ItemDecoration() {
private val mapping = SparseIntArray(spacingMapping.size)
init {
spacingMapping.forEach { (k, v) -> mapping[k] = v }
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val itemType = parent.getChildViewHolder(view)?.itemViewType
val spacing = if (itemType == null) {
fallbackSpacing
} else {
mapping.getOrDefault(itemType, fallbackSpacing)
}
outRect.set(spacing, spacing, spacing, spacing)
}
}

View File

@@ -0,0 +1,82 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.measureWidth
import kotlin.math.hypot
class BubbleAnimator(
private val bubble: View,
) {
private val animationDuration = (bubble.resources.getInteger(android.R.integer.config_shortAnimTime) *
bubble.context.animatorDurationScale).toLong()
private var animator: Animator? = null
private var isHiding = false
fun show() {
if (bubble.isVisible && !isHiding) {
return
}
isHiding = false
animator?.cancel()
animator = ViewAnimationUtils.createCircularReveal(
bubble,
bubble.measureWidth(),
bubble.measuredHeight,
0f,
hypot(bubble.width.toDouble(), bubble.height.toDouble()).toFloat(),
).apply {
bubble.isVisible = true
duration = animationDuration
interpolator = DecelerateInterpolator()
start()
}
}
fun hide() {
if (!bubble.isVisible || isHiding) {
return
}
animator?.cancel()
isHiding = true
animator = ViewAnimationUtils.createCircularReveal(
bubble,
bubble.width,
bubble.height,
hypot(bubble.width.toDouble(), bubble.height.toDouble()).toFloat(),
0f,
).apply {
duration = animationDuration
interpolator = AccelerateInterpolator()
addListener(HideListener())
start()
}
}
private inner class HideListener : AnimatorListenerAdapter() {
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@BubbleAnimator.animator) {
bubble.isInvisible = true
isHiding = false
this@BubbleAnimator.animator = null
}
}
}
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
class FastScrollRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = androidx.recyclerview.R.attr.recyclerViewStyle,
) : RecyclerView(context, attrs, defStyleAttr) {
val fastScroller = FastScroller(context, attrs)
init {
fastScroller.id = R.id.fast_scroller
fastScroller.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(adapter)
fastScroller.setSectionIndexer(adapter as? FastScroller.SectionIndexer)
}
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
fastScroller.visibility = visibility
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastScroller.attachRecyclerView(this)
}
override fun onDetachedFromWindow() {
fastScroller.detachRecyclerView()
super.onDetachedFromWindow()
}
}

View File

@@ -0,0 +1,521 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import androidx.core.view.GravityCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.FastScrollerBinding
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.isLayoutReversed
import kotlin.math.roundToInt
import com.google.android.material.R as materialR
private const val SCROLLBAR_HIDE_DELAY = 1000L
private const val TRACK_SNAP_RANGE = 5
@Suppress("MemberVisibilityCanBePrivate", "unused")
class FastScroller @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = R.attr.fastScrollerStyle,
) : LinearLayout(context, attrs, defStyleAttr) {
enum class BubbleSize(@DrawableRes val drawableId: Int, @DimenRes val textSizeId: Int) {
NORMAL(R.drawable.fastscroll_bubble, R.dimen.fastscroll_bubble_text_size),
SMALL(R.drawable.fastscroll_bubble_small, R.dimen.fastscroll_bubble_text_size_small)
}
private val binding = FastScrollerBinding.inflate(LayoutInflater.from(context), this)
private val scrollbarPaddingEnd = context.resources.getDimension(R.dimen.fastscroll_scrollbar_padding_end)
@ColorInt
private var bubbleColor = 0
@ColorInt
private var handleColor = 0
private var bubbleHeight = 0
private var handleHeight = 0
private var viewHeight = 0
private var hideScrollbar = true
private var showBubble = true
private var showBubbleAlways = false
private var bubbleSize = BubbleSize.NORMAL
private var bubbleImage: Drawable? = null
private var handleImage: Drawable? = null
private var trackImage: Drawable? = null
private var recyclerView: RecyclerView? = null
private val scrollbarAnimator = ScrollbarAnimator(binding.scrollbar, scrollbarPaddingEnd)
private val bubbleAnimator = BubbleAnimator(binding.bubble)
private var fastScrollListener: FastScrollListener? = null
private var sectionIndexer: SectionIndexer? = null
private val scrollbarHider = Runnable {
hideBubble()
hideScrollbar()
}
private val scrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!binding.thumb.isSelected && isEnabled) {
val y = recyclerView.scrollProportion
setViewPositions(y)
if (showBubbleAlways) {
val targetPos = getRecyclerViewTargetPosition(y)
sectionIndexer?.let { binding.bubble.text = it.getSectionText(recyclerView.context, targetPos) }
}
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (isEnabled) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
handler.removeCallbacks(scrollbarHider)
showScrollbar()
if (showBubbleAlways && sectionIndexer != null) showBubble()
}
RecyclerView.SCROLL_STATE_IDLE -> if (hideScrollbar && !binding.thumb.isSelected) {
handler.postDelayed(scrollbarHider, SCROLLBAR_HIDE_DELAY)
}
}
}
}
}
private val RecyclerView.scrollProportion: Float
get() {
val rangeDiff = computeVerticalScrollRange() - computeVerticalScrollExtent()
val proportion = computeVerticalScrollOffset() / if (rangeDiff > 0) rangeDiff.toFloat() else 1f
return viewHeight * proportion
}
init {
clipChildren = false
orientation = HORIZONTAL
@ColorInt var bubbleColor = context.getThemeColor(materialR.attr.colorControlNormal, Color.DKGRAY)
@ColorInt var handleColor = bubbleColor
@ColorInt var trackColor = context.getThemeColor(materialR.attr.colorOutline, Color.LTGRAY)
@ColorInt var textColor = context.getThemeColor(android.R.attr.textColorPrimaryInverse, Color.WHITE)
var showTrack = false
context.withStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr) {
bubbleColor = getColor(R.styleable.FastScroller_bubbleColor, bubbleColor)
handleColor = getColor(R.styleable.FastScroller_thumbColor, handleColor)
trackColor = getColor(R.styleable.FastScroller_trackColor, trackColor)
textColor = getColor(R.styleable.FastScroller_bubbleTextColor, textColor)
hideScrollbar = getBoolean(R.styleable.FastScroller_hideScrollbar, hideScrollbar)
showBubble = getBoolean(R.styleable.FastScroller_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScroller_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScroller_showTrack, showTrack)
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
}
setTrackColor(trackColor)
setHandleColor(handleColor)
setBubbleColor(bubbleColor)
setBubbleTextColor(textColor)
setHideScrollbar(hideScrollbar)
setBubbleVisible(showBubble, showBubbleAlways)
setTrackVisible(showTrack)
}
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
super.onSizeChanged(w, h, oldW, oldH)
viewHeight = h
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
val setYPositions: () -> Unit = {
val y = event.y
setViewPositions(y)
setRecyclerViewPosition(y)
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false
requestDisallowInterceptTouchEvent(true)
setHandleSelected(true)
handler.removeCallbacks(scrollbarHider)
showScrollbar()
if (showBubble && sectionIndexer != null) showBubble()
fastScrollListener?.onFastScrollStart(this)
setYPositions()
return true
}
MotionEvent.ACTION_MOVE -> {
setYPositions()
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
requestDisallowInterceptTouchEvent(false)
setHandleSelected(false)
if (hideScrollbar) handler.postDelayed(scrollbarHider, SCROLLBAR_HIDE_DELAY)
if (!showBubbleAlways) hideBubble()
fastScrollListener?.onFastScrollStop(this)
return true
}
}
return super.onTouchEvent(event)
}
/**
* Set the enabled state of this view.
*
* @param enabled True if this view is enabled, false otherwise
*/
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
isVisible = enabled
}
/**
* Set the [ViewGroup.LayoutParams] associated with this view. These supply
* parameters to the *parent* of this view specifying how it should be arranged.
*
* @param params The [ViewGroup.LayoutParams] for this view, cannot be null
*/
override fun setLayoutParams(params: ViewGroup.LayoutParams) {
params.width = LayoutParams.WRAP_CONTENT
super.setLayoutParams(params)
}
/**
* Set the [ViewGroup.LayoutParams] associated with this view. These supply
* parameters to the *parent* of this view specifying how it should be arranged.
*
* @param viewGroup The parent [ViewGroup] for this view, cannot be null
*/
fun setLayoutParams(viewGroup: ViewGroup) {
val recyclerViewId = recyclerView?.id ?: NO_ID
val marginTop = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top)
val marginBottom = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom)
require(recyclerViewId != NO_ID) { "RecyclerView must have a view ID" }
when (viewGroup) {
is ConstraintLayout -> {
val endId = if (recyclerView?.parent === parent) recyclerViewId else ConstraintSet.PARENT_ID
val startId = id
ConstraintSet().apply {
clone(viewGroup)
connect(startId, ConstraintSet.TOP, endId, ConstraintSet.TOP)
connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.BOTTOM)
connect(startId, ConstraintSet.END, endId, ConstraintSet.END)
applyTo(viewGroup)
}
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
height = 0
setMargins(0, marginTop, 0, marginBottom)
}
}
is CoordinatorLayout -> layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply {
height = LayoutParams.MATCH_PARENT
anchorGravity = GravityCompat.END
anchorId = recyclerViewId
setMargins(0, marginTop, 0, marginBottom)
}
is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply {
height = LayoutParams.MATCH_PARENT
gravity = GravityCompat.END
setMargins(0, marginTop, 0, marginBottom)
}
is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply {
height = 0
addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
addRule(RelativeLayout.ALIGN_END, recyclerViewId)
setMargins(0, marginTop, 0, marginBottom)
}
else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout")
}
updateViewHeights()
}
/**
* Set the [RecyclerView] associated with this [FastScroller]. This allows the
* FastScroller to set its layout parameters and listen for scroll changes.
*
* @param recyclerView The [RecyclerView] to attach, cannot be null
* @see detachRecyclerView
*/
fun attachRecyclerView(recyclerView: RecyclerView) {
if (this.recyclerView != null) {
detachRecyclerView()
}
this.recyclerView = recyclerView
if (parent is ViewGroup) {
setLayoutParams(parent as ViewGroup)
} else if (recyclerView.parent is ViewGroup) {
val viewGroup = recyclerView.parent as ViewGroup
viewGroup.addView(this)
setLayoutParams(viewGroup)
}
recyclerView.addOnScrollListener(scrollListener)
// set initial positions for bubble and thumb
post { setViewPositions(this.recyclerView?.scrollProportion ?: 0f) }
}
/**
* Clears references to the attached [RecyclerView] and stops listening for scroll changes.
*
* @see attachRecyclerView
*/
fun detachRecyclerView() {
recyclerView?.removeOnScrollListener(scrollListener)
recyclerView = null
}
/**
* Set a new [FastScrollListener] that will listen to fast scroll events.
*
* @param fastScrollListener The new [FastScrollListener] to set, or null to set none
*/
fun setFastScrollListener(fastScrollListener: FastScrollListener?) {
this.fastScrollListener = fastScrollListener
}
/**
* Set a new [SectionIndexer] that provides section text for this [FastScroller].
*
* @param sectionIndexer The new [SectionIndexer] to set, or null to set none
*/
fun setSectionIndexer(sectionIndexer: SectionIndexer?) {
this.sectionIndexer = sectionIndexer
}
/**
* Hide the scrollbar when not scrolling.
*
* @param hideScrollbar True to hide the scrollbar, false to show
*/
fun setHideScrollbar(hideScrollbar: Boolean) {
if (this.hideScrollbar != hideScrollbar) {
this.hideScrollbar = hideScrollbar
binding.scrollbar.isGone = hideScrollbar
}
}
/**
* Show the scroll track while scrolling.
*
* @param visible True to show scroll track, false to hide
*/
fun setTrackVisible(visible: Boolean) {
binding.track.isVisible = visible
}
/**
* Set the color of the scroll track.
*
* @param color The color for the scroll track
*/
fun setTrackColor(@ColorInt color: Int) {
if (trackImage == null) {
trackImage = ContextCompat.getDrawable(context, R.drawable.fastscroll_track)
}
trackImage?.let {
it.setTint(color)
binding.track.setImageDrawable(it)
}
}
/**
* Set the color of the scroll thumb.
*
* @param color The color for the scroll thumb
*/
fun setHandleColor(@ColorInt color: Int) {
handleColor = color
if (handleImage == null) {
handleImage = ContextCompat.getDrawable(context, R.drawable.fastscroll_handle)
}
handleImage?.let {
it.setTint(handleColor)
binding.thumb.setImageDrawable(it)
}
}
/**
* Show the section bubble while scrolling.
*
* @param visible True to show the bubble, false to hide
* @param always True to always show the bubble, false to only show on thumb touch
*/
@JvmOverloads
fun setBubbleVisible(visible: Boolean, always: Boolean = false) {
showBubble = visible
showBubbleAlways = visible && always
}
/**
* Set the background color of the section bubble.
*
* @param color The background color for the section bubble
*/
fun setBubbleColor(@ColorInt color: Int) {
bubbleColor = color
if (bubbleImage == null) {
bubbleImage = ContextCompat.getDrawable(context, bubbleSize.drawableId)
}
bubbleImage?.let {
it.setTint(bubbleColor)
binding.bubble.background = it
}
}
/**
* Set the text color of the section bubble.
*
* @param color The text color for the section bubble
*/
fun setBubbleTextColor(@ColorInt color: Int) = binding.bubble.setTextColor(color)
/**
* Set the scaled pixel text size of the section bubble.
*
* @param size The scaled pixel text size for the section bubble
*/
fun setBubbleTextSize(size: Int) {
binding.bubble.textSize = size.toFloat()
}
private fun getRecyclerViewTargetPosition(y: Float) = recyclerView?.let { recyclerView ->
val itemCount = recyclerView.adapter?.itemCount ?: 0
val proportion = when {
binding.thumb.y == 0f -> 0f
binding.thumb.y + handleHeight >= viewHeight - TRACK_SNAP_RANGE -> 1f
else -> y / viewHeight.toFloat()
}
var scrolledItemCount = (proportion * itemCount).roundToInt()
if (recyclerView.layoutManager.isLayoutReversed) {
scrolledItemCount = itemCount - scrolledItemCount
}
if (itemCount > 0) scrolledItemCount.coerceIn(0, itemCount - 1) else 0
} ?: 0
private fun setRecyclerViewPosition(y: Float) {
val layoutManager = recyclerView?.layoutManager ?: return
val targetPos = getRecyclerViewTargetPosition(y)
layoutManager.scrollToPosition(targetPos)
if (showBubble) sectionIndexer?.let { binding.bubble.text = it.getSectionText(context, targetPos) }
}
private fun setViewPositions(y: Float) {
bubbleHeight = binding.bubble.measuredHeight
handleHeight = binding.thumb.measuredHeight
val bubbleHandleHeight = bubbleHeight + handleHeight / 2f
if (showBubble && viewHeight >= bubbleHandleHeight) {
binding.bubble.y = (y - bubbleHeight).coerceIn(0f, viewHeight - bubbleHandleHeight)
}
if (viewHeight >= handleHeight) {
binding.thumb.y = (y - handleHeight / 2).coerceIn(0f, viewHeight - handleHeight.toFloat())
}
}
private fun updateViewHeights() {
val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
binding.bubble.measure(measureSpec, measureSpec)
bubbleHeight = binding.bubble.measuredHeight
binding.thumb.measure(measureSpec, measureSpec)
handleHeight = binding.thumb.measuredHeight
}
private fun showBubble() {
bubbleAnimator.show()
}
private fun hideBubble() {
bubbleAnimator.hide()
}
private fun showScrollbar() {
if (recyclerView?.run { canScrollVertically(1) || canScrollVertically(-1) } == true) {
scrollbarAnimator.show()
}
}
private fun hideScrollbar() {
scrollbarAnimator.hide()
}
private fun setHandleSelected(selected: Boolean) {
binding.thumb.isSelected = selected
handleImage?.setTint(if (selected) bubbleColor else handleColor)
}
private fun TypedArray.getBubbleSize(@StyleableRes index: Int, defaultValue: BubbleSize): BubbleSize {
val ordinal = getInt(index, -1)
return BubbleSize.values().getOrNull(ordinal) ?: defaultValue
}
private val BubbleSize.textSize
@Px get() = resources.getDimension(textSizeId)
interface FastScrollListener {
fun onFastScrollStart(fastScroller: FastScroller)
fun onFastScrollStop(fastScroller: FastScroller)
}
interface SectionIndexer {
fun getSectionText(context: Context, position: Int): CharSequence
}
}

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.base.ui.list.fastscroll
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewPropertyAnimator
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
class ScrollbarAnimator(
private val scrollbar: View,
private val scrollbarPaddingEnd: Float,
) {
private val animationDuration = (scrollbar.resources.getInteger(R.integer.config_defaultAnimTime) *
scrollbar.context.animatorDurationScale).toLong()
private var animator: ViewPropertyAnimator? = null
private var isHiding = false
fun show() {
if (scrollbar.isVisible && !isHiding) {
return
}
isHiding = false
animator?.cancel()
scrollbar.translationX = scrollbarPaddingEnd
scrollbar.isVisible = true
animator = scrollbar
.animate()
.translationX(0f)
.alpha(1f)
.setDuration(animationDuration)
}
fun hide() {
if (!scrollbar.isVisible || isHiding) {
return
}
animator?.cancel()
isHiding = true
animator = scrollbar
.animate()
.translationX(scrollbarPaddingEnd)
.alpha(0f)
.setDuration(animationDuration)
.setListener(HideListener())
}
private inner class HideListener : AnimatorListenerAdapter() {
private var isCancelled = false
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
isCancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
if (!isCancelled && animation === this@ScrollbarAnimator.animator) {
scrollbar.isInvisible = true
isHiding = false
this@ScrollbarAnimator.animator = null
}
}
}
}

View File

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class ActionModeDelegate {
private var activeActionMode: ActionMode? = null
private var listeners: MutableList<ActionModeListener>? = null
val isActionModeStarted: Boolean
get() = activeActionMode != null
fun onSupportActionModeStarted(mode: ActionMode) {
activeActionMode = mode
listeners?.forEach { it.onActionModeStarted(mode) }
}
fun onSupportActionModeFinished(mode: ActionMode) {
activeActionMode = null
listeners?.forEach { it.onActionModeFinished(mode) }
}
fun addListener(listener: ActionModeListener) {
if (listeners == null) {
listeners = ArrayList()
}
checkNotNull(listeners).add(listener)
}
fun removeListener(listener: ActionModeListener) {
listeners?.remove(listener)
}
fun addListener(listener: ActionModeListener, owner: LifecycleOwner) {
addListener(listener)
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
}
private inner class ListenerLifecycleObserver(
private val listener: ActionModeListener,
) : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
removeListener(listener)
}
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.appcompat.view.ActionMode
interface ActionModeListener {
fun onActionModeStarted(mode: ActionMode)
fun onActionModeFinished(mode: ActionMode)
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.base.ui.util
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallbacks {
private val activities = WeakHashMap<Activity, Unit>()
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activities[activity] = Unit
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity)
}
fun recreateAll() {
val snapshot = activities.keys.toList()
snapshot.forEach { it.recreate() }
}
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.base.ui.util
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}
// Hilt cannot inject into parametrized classes
fun BaseActivityEntryPoint.inject(activity: BaseActivity<*>) {
activity.settings = settings
}

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.annotation.AnyThread
import androidx.lifecycle.LiveData
import java.util.concurrent.atomic.AtomicInteger
class CountedBooleanLiveData : LiveData<Boolean>(false) {
private val counter = AtomicInteger(0)
@AnyThread
fun increment() {
if (counter.getAndIncrement() == 0) {
postValue(true)
}
}
@AnyThread
fun decrement() {
if (counter.decrementAndGet() == 0) {
postValue(false)
}
}
@AnyThread
fun reset() {
if (counter.getAndSet(0) != 0) {
postValue(false)
}
}
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.annotation.StringRes
import org.koitharu.kotatsu.base.domain.ReversibleHandle
class ReversibleAction(
@StringRes val stringResId: Int,
val handle: ReversibleHandle?,
)

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.base.ui.util
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior
import androidx.core.view.ViewCompat
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
class ShrinkOnScrollBehavior : Behavior<ExtendedFloatingActionButton> {
@Suppress("unused") constructor() : super()
@Suppress("unused") constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: ExtendedFloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: ExtendedFloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
if (dyConsumed > 0) {
if (child.isExtended) {
child.shrink()
}
} else if (dyConsumed < 0) {
if (!child.isExtended) {
child.extend()
}
}
}
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.base.ui.util
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
private var animator: ValueAnimator? = null
private val interpolator = AccelerateDecelerateInterpolator()
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val foreground = appBarLayout.statusBarForeground ?: return
val start = foreground.alpha
val collapsed = verticalOffset != 0
val end = if (collapsed) 255 else 0
animator?.cancel()
if (start == end) {
animator = null
return
}
animator = ValueAnimator.ofInt(start, end).apply {
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
interpolator = this@StatusBarDimHelper.interpolator
addUpdateListener {
foreground.alpha = it.animatedValue as Int
}
start()
}
}
fun attachToAppBar(appBarLayout: AppBarLayout) {
appBarLayout.addOnOffsetChangedListener(this)
appBarLayout.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
alpha = 0
}
}
}

View File

@@ -16,10 +16,7 @@ class WindowInsetsDelegate(
private var lastInsets: Insets? = null
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? {
if (insets == null) {
return null
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets
val newInsets = if (handleImeInsets) {
Insets.max(
@@ -49,7 +46,7 @@ class WindowInsetsDelegate(
) {
view.removeOnLayoutChangeListener(this)
if (lastInsets == null) { // Listener may not be called
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view))
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view) ?: return)
}
}

View File

@@ -0,0 +1,282 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes
import androidx.core.view.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import java.util.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderBinding
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import org.koitharu.kotatsu.utils.ext.parents
class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = materialR.attr.appBarLayoutStyle,
) : AppBarLayout(context, attrs, defStyleAttr), MenuHost {
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
private val bottomSheetCallback = Callback()
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList<OnExpansionChangeListener>()
private var fitStatusBar = false
private val minHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_min)
private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max)
private var isLayoutSuppressedCompat = false
private var isLayoutCalledWhileSuppressed = false
@Deprecated("")
val toolbar: MaterialToolbar
get() = binding.toolbar
var title: CharSequence?
get() = binding.toolbar.title
set(value) {
binding.toolbar.title = value
}
var subtitle: CharSequence?
get() = binding.toolbar.subtitle
set(value) {
binding.toolbar.subtitle = value
}
init {
setBackgroundResource(R.drawable.sheet_toolbar_background)
layoutTransition = LayoutTransition().apply {
setDuration(context.getAnimationDuration(R.integer.config_tinyAnimTime))
}
context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) {
binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title)
fitStatusBar = getBoolean(R.styleable.BottomSheetHeaderBar_fitStatusBar, fitStatusBar)
val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0)
if (menuResId != 0) {
binding.toolbar.inflateMenu(menuResId)
}
}
binding.toolbar.setNavigationOnClickListener(bottomSheetCallback)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setBottomSheetBehavior(findParentBottomSheetBehavior())
}
override fun onDetachedFromWindow() {
setBottomSheetBehavior(null)
super.onDetachedFromWindow()
}
override fun addView(child: View?, index: Int) {
if (shouldAddView(child)) {
super.addView(child, index)
} else {
binding.toolbar.addView(child, index)
}
}
override fun addView(child: View?, width: Int, height: Int) {
if (shouldAddView(child)) {
super.addView(child, width, height)
} else {
binding.toolbar.addView(child, width, height)
}
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (shouldAddView(child)) {
super.addView(child, index, params)
} else {
binding.toolbar.addView(child, index, convertLayoutParams(params))
}
}
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null)
return super.onApplyWindowInsets(insets)
}
override fun addMenuProvider(provider: MenuProvider) {
binding.toolbar.addMenuProvider(provider)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner) {
binding.toolbar.addMenuProvider(provider, owner)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner, state: Lifecycle.State) {
binding.toolbar.addMenuProvider(provider, owner, state)
}
override fun removeMenuProvider(provider: MenuProvider) {
binding.toolbar.removeMenuProvider(provider)
}
override fun invalidateMenu() {
binding.toolbar.invalidateMenu()
}
fun setNavigationOnClickListener(onClickListener: OnClickListener) {
binding.toolbar.setNavigationOnClickListener(onClickListener)
}
fun addOnExpansionChangeListener(listener: OnExpansionChangeListener) {
expansionListeners.add(listener)
}
fun removeOnExpansionChangeListener(listener: OnExpansionChangeListener) {
expansionListeners.remove(listener)
}
fun setTitle(@StringRes resId: Int) {
binding.toolbar.setTitle(resId)
}
fun setSubtitle(@StringRes resId: Int) {
binding.toolbar.setSubtitle(resId)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (isLayoutSuppressedCompat) {
isLayoutCalledWhileSuppressed = true
} else {
super.onLayout(changed, l, t, r, b)
}
}
private fun setBottomSheetBehavior(behavior: BottomSheetBehavior<*>?) {
bottomSheetBehavior?.removeBottomSheetCallback(bottomSheetCallback)
bottomSheetBehavior = behavior
if (behavior != null) {
onBottomSheetStateChanged(behavior.state)
behavior.addBottomSheetCallback(bottomSheetCallback)
}
}
private fun onBottomSheetStateChanged(newState: Int) {
val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
if (isExpanded == binding.dragHandle.isGone) {
return
}
suppressLayoutCompat(true)
binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null)
binding.dragHandle.isGone = isExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, isExpanded) }
dispatchInsets(ViewCompat.getRootWindowInsets(this))
suppressLayoutCompat(false)
}
private fun suppressLayoutCompat(suppress: Boolean) {
if (suppress == isLayoutSuppressedCompat) return
isLayoutSuppressedCompat = suppress
if (!suppress && isLayoutCalledWhileSuppressed) {
requestLayout()
}
isLayoutCalledWhileSuppressed = false
}
private fun dispatchInsets(insets: WindowInsetsCompat?) {
if (!fitStatusBar) {
return
}
val isExpanded = binding.dragHandle.isGone
val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
if (isExpanded) {
updatePadding(top = topInset)
} else {
updatePadding(top = 0)
binding.dragHandle.updateLayoutParams {
height = topInset.coerceIn(minHandleHeight, maxHandleHeight)
}
}
}
private fun findParentBottomSheetBehavior(): BottomSheetBehavior<*>? {
for (p in parents) {
val layoutParams = (p as? View)?.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is BottomSheetBehavior<*>) {
return behavior
}
}
}
return null
}
private fun isOnTopOfScreen(): Boolean {
getLocationInWindow(locationBuffer)
val topInset = ViewCompat.getRootWindowInsets(this)
?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
val zeroTop = (layoutParams as? MarginLayoutParams)?.topMargin ?: 0
return (locationBuffer[1] - topInset) <= zeroTop
}
private fun dismissBottomSheet() {
val behavior = bottomSheetBehavior ?: return
if (behavior.isHideable) {
behavior.state = BottomSheetBehavior.STATE_HIDDEN
} else {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
private fun shouldAddView(child: View?): Boolean {
if (child == null) {
return true
}
val viewId = child.id
return viewId == R.id.dragHandle || viewId == R.id.toolbar
}
private fun convertLayoutParams(params: ViewGroup.LayoutParams?): Toolbar.LayoutParams? {
return when (params) {
null -> null
is MarginLayoutParams -> {
val lp = Toolbar.LayoutParams(params)
if (params is LayoutParams) {
lp.gravity = params.gravity
}
lp
}
else -> Toolbar.LayoutParams(params)
}
}
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onBottomSheetStateChanged(newState)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
override fun onClick(v: View?) {
dismissBottomSheet()
}
}
fun interface OnExpansionChangeListener {
fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean)
}
}

View File

@@ -8,23 +8,33 @@ import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.annotation.IdRes
import androidx.core.view.children
import com.google.android.material.R as materialR
import com.google.android.material.button.MaterialButton
import com.google.android.material.shape.ShapeAppearanceModel
class CheckableButtonGroup @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener {
@AttrRes defStyleAttr: Int = materialR.attr.materialButtonToggleGroupStyle,
) : LinearLayout(context, attrs, defStyleAttr, materialR.style.Widget_MaterialComponents_MaterialButtonToggleGroup),
View.OnClickListener {
private val originalCornerData = ArrayList<CornerData>()
var onCheckedChangeListener: OnCheckedChangeListener? = null
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (child is MaterialButton) {
child.setOnClickListener(this)
setupButton(child)
}
super.addView(child, index, params)
}
override fun onFinishInflate() {
super.onFinishInflate()
updateChildShapes()
}
override fun onClick(v: View) {
setCheckedId(v.id)
}
@@ -36,7 +46,74 @@ class CheckableButtonGroup @JvmOverloads constructor(
onCheckedChangeListener?.onCheckedChanged(this, viewRes)
}
private fun updateChildShapes() {
val childCount = childCount
val firstVisibleChildIndex = 0
val lastVisibleChildIndex = childCount - 1
for (i in 0 until childCount) {
val button: MaterialButton = getChildAt(i) as? MaterialButton ?: continue
if (button.visibility == GONE) {
continue
}
val builder = button.shapeAppearanceModel.toBuilder()
val newCornerData: CornerData? =
getNewCornerData(i, firstVisibleChildIndex, lastVisibleChildIndex)
updateBuilderWithCornerData(builder, newCornerData)
button.shapeAppearanceModel = builder.build()
}
}
private fun setupButton(button: MaterialButton) {
button.setOnClickListener(this)
button.isElegantTextHeight = false
// Saves original corner data
val shapeAppearanceModel: ShapeAppearanceModel = button.shapeAppearanceModel
originalCornerData.add(
CornerData(
shapeAppearanceModel.topLeftCornerSize,
shapeAppearanceModel.bottomLeftCornerSize,
shapeAppearanceModel.topRightCornerSize,
shapeAppearanceModel.bottomRightCornerSize,
),
)
}
private fun getNewCornerData(
index: Int,
firstVisibleChildIndex: Int,
lastVisibleChildIndex: Int,
): CornerData? {
val cornerData: CornerData = originalCornerData.get(index)
// If only one (visible) child exists, use its original corners
if (firstVisibleChildIndex == lastVisibleChildIndex) {
return cornerData
}
val isHorizontal = orientation == HORIZONTAL
if (index == firstVisibleChildIndex) {
return if (isHorizontal) cornerData.start(this) else cornerData.top()
}
return if (index == lastVisibleChildIndex) {
if (isHorizontal) cornerData.end(this) else cornerData.bottom()
} else null
}
private fun updateBuilderWithCornerData(
shapeAppearanceModelBuilder: ShapeAppearanceModel.Builder,
cornerData: CornerData?,
) {
if (cornerData == null) {
shapeAppearanceModelBuilder.setAllCornerSizes(0f)
return
}
shapeAppearanceModelBuilder
.setTopLeftCornerSize(cornerData.topLeft)
.setBottomLeftCornerSize(cornerData.bottomLeft)
.setTopRightCornerSize(cornerData.topRight)
.setBottomRightCornerSize(cornerData.bottomRight)
}
fun interface OnCheckedChangeListener {
fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
}
}
}

View File

@@ -5,10 +5,12 @@ import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.ParcelCompat
import androidx.customview.view.AbsSavedState
class CheckableImageView @JvmOverloads constructor(
context: Context,
@@ -61,12 +63,18 @@ class CheckableImageView @JvmOverloads constructor(
}
}
class ToggleOnClickListener : OnClickListener {
override fun onClick(view: View) {
(view as? Checkable)?.toggle()
}
}
fun interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
}
private class SavedState : BaseSavedState {
private class SavedState : AbsSavedState {
val isChecked: Boolean
@@ -74,7 +82,7 @@ class CheckableImageView @JvmOverloads constructor(
isChecked = checked
}
constructor(source: Parcel) : super(source) {
constructor(source: Parcel, classLoader: ClassLoader?) : super(source, classLoader) {
isChecked = ParcelCompat.readBoolean(source)
}
@@ -84,9 +92,10 @@ class CheckableImageView @JvmOverloads constructor(
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Creator<SavedState> = object : Creator<SavedState> {
override fun createFromParcel(`in`: Parcel) = SavedState(`in`)
override fun createFromParcel(`in`: Parcel) = SavedState(`in`, SavedState::class.java.classLoader)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}

View File

@@ -5,23 +5,26 @@ import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.view.children
import com.google.android.material.R as materialR
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.castOrNull
import org.koitharu.kotatsu.utils.ext.getThemeColorStateList
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle,
) : ChipGroup(context, attrs, defStyleAttr) {
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private var chipOnClickListener = OnClickListener {
private val chipOnClickListener = OnClickListener {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
private var chipOnCloseListener = OnClickListener {
private val chipOnCloseListener = OnClickListener {
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
}
var onChipClickListener: OnChipClickListener? = null
@@ -60,15 +63,27 @@ class ChipsView @JvmOverloads constructor(
}
}
fun <T> getCheckedData(cls: Class<T>): Set<T> {
val result = LinkedHashSet<T>(childCount)
for (child in children) {
if (child is Chip && child.isChecked) {
result += cls.castOrNull(child.tag) ?: continue
}
}
return result
}
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
if (model.icon == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.isChipIconVisible = true
chip.setChipIconResource(model.icon)
}
chip.isClickable = onChipClickListener != null
chip.isClickable = onChipClickListener != null || model.isCheckable
chip.isCheckable = model.isCheckable
chip.isChecked = model.isChecked
chip.tag = model.data
}
@@ -76,11 +91,13 @@ class ChipsView @JvmOverloads constructor(
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.setCheckedIconResource(R.drawable.ic_check)
chip.checkedIconTint = context.getThemeColorStateList(materialR.attr.colorControlNormal)
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isCheckable = false
addView(chip)
return chip
}
@@ -98,7 +115,9 @@ class ChipsView @JvmOverloads constructor(
class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
val isCheckable: Boolean,
val isChecked: Boolean,
val data: Any? = null,
) {
override fun equals(other: Any?): Boolean {
@@ -109,6 +128,8 @@ class ChipsView @JvmOverloads constructor(
if (icon != other.icon) return false
if (title != other.title) return false
if (isCheckable != other.isCheckable) return false
if (isChecked != other.isChecked) return false
if (data != other.data) return false
return true
@@ -117,7 +138,9 @@ class ChipsView @JvmOverloads constructor(
override fun hashCode(): Int {
var result = icon
result = 31 * result + title.hashCode()
result = 31 * result + data.hashCode()
result = 31 * result + isCheckable.hashCode()
result = 31 * result + isChecked.hashCode()
result = 31 * result + (data?.hashCode() ?: 0)
return result
}
}
@@ -131,4 +154,4 @@ class ChipsView @JvmOverloads constructor(
fun onChipCloseClick(chip: Chip, data: Any?)
}
}
}

View File

@@ -0,0 +1,47 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.view.View
import androidx.core.view.ViewCompat
import com.google.android.material.shape.AbsoluteCornerSize
import com.google.android.material.shape.CornerSize
class CornerData(
var topLeft: CornerSize,
var bottomLeft: CornerSize,
var topRight: CornerSize,
var bottomRight: CornerSize,
) {
fun start(view: View): CornerData {
return if (isLayoutRtl(view)) right() else left()
}
fun end(view: View): CornerData {
return if (isLayoutRtl(view)) left() else right()
}
fun left(): CornerData {
return CornerData(topLeft, bottomLeft, noCorner, noCorner)
}
fun right(): CornerData {
return CornerData(noCorner, noCorner, topRight, bottomRight)
}
fun top(): CornerData {
return CornerData(topLeft, noCorner, topRight, noCorner)
}
fun bottom(): CornerData {
return CornerData(noCorner, bottomLeft, noCorner, bottomRight)
}
private companion object {
val noCorner: CornerSize = AbsoluteCornerSize(0f)
fun isLayoutRtl(view: View): Boolean {
return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL
}
}
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.Button
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.postDelayed
import org.koitharu.kotatsu.R
private const val ENTER_DURATION = 300L
private const val EXIT_DURATION = 200L
private const val SHORT_DURATION = 1_500L
private const val LONG_DURATION = 2_750L
/**
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
*
* Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
*
* https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
*/
class FadingSnackbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val message: TextView
private val action: Button
init {
val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
message = view.findViewById(R.id.snackbar_text)
action = view.findViewById(R.id.snackbar_action)
}
fun dismiss() {
if (visibility == VISIBLE && alpha == 1f) {
animate()
.alpha(0f)
.withEndAction { visibility = GONE }
.duration = EXIT_DURATION
}
}
fun show(
messageText: CharSequence? = null,
@StringRes actionId: Int? = null,
longDuration: Boolean = true,
actionClick: () -> Unit = { dismiss() },
dismissListener: () -> Unit = { }
) {
message.text = messageText
if (actionId != null) {
action.run {
visibility = VISIBLE
text = context.getString(actionId)
setOnClickListener {
actionClick()
}
}
} else {
action.visibility = GONE
}
alpha = 0f
visibility = VISIBLE
animate()
.alpha(1f)
.duration = ENTER_DURATION
val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
postDelayed(showDuration) {
dismiss()
dismissListener()
}
}
}

View File

@@ -0,0 +1,101 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.measureHeight
class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
context: Context? = null,
attrs: AttributeSet? = null,
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
@ViewCompat.NestedScrollType
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
private var dyRatio = 1F
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
return dependency is AppBarLayout
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: BottomNavigationView,
dependency: View,
): Boolean {
val appBarSize = dependency.measureHeight()
dyRatio = if (appBarSize > 0) {
child.measureHeight().toFloat() / appBarSize
} else {
1F
}
return false
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: BottomNavigationView,
directTargetChild: View,
target: View,
axes: Int,
type: Int,
): Boolean {
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
return false
}
lastStartedType = type
offsetAnimator?.cancel()
return true
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: BottomNavigationView,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int,
) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
}
override fun onStopNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: BottomNavigationView,
target: View,
type: Int,
) {
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
}
}
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
offsetAnimator?.cancel()
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = child.context.getAnimationDuration(R.integer.config_shorterAnimTime)
addUpdateListener {
child.translationY = it.animatedValue as Float
}
}
offsetAnimator?.setFloatValues(
child.translationY,
if (isVisible) 0F else child.height.toFloat(),
)
offsetAnimator?.start()
}
}

View File

@@ -9,16 +9,17 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.graphics.drawable.shapes.RoundRectShape
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatCheckedTextView
import androidx.core.content.res.use
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import com.google.android.material.ripple.RippleUtils
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.resolveDp
@SuppressLint("RestrictedApi")
class ListItemTextView @JvmOverloads constructor(
@@ -36,13 +37,13 @@ class ListItemTextView @JvmOverloads constructor(
init {
context.withStyledAttributes(attrs, R.styleable.ListItemTextView, defStyleAttr) {
val itemRippleColor = getColorStateList(R.styleable.ListItemTextView_rippleColor)
?: getRippleColorFallback(context)
val itemRippleColor = getRippleColor(context)
val shape = createShapeDrawable(this)
val roundCorners = FloatArray(8) { resources.resolveDp(32f) }
background = RippleDrawable(
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor),
shape,
ShapeDrawable(RectShape()),
ShapeDrawable(RoundRectShape(roundCorners, null, null)),
)
checkedDrawableStart = getDrawable(R.styleable.ListItemTextView_checkedDrawableStart)
checkedDrawableEnd = getDrawable(R.styleable.ListItemTextView_checkedDrawableEnd)
@@ -108,7 +109,7 @@ class ListItemTextView @JvmOverloads constructor(
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearanceOverlay, 0),
).build()
val shapeDrawable = MaterialShapeDrawable(shapeAppearance)
shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundTint)
shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundFillColor)
return InsetDrawable(
shapeDrawable,
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetLeft, 0),
@@ -118,9 +119,8 @@ class ListItemTextView @JvmOverloads constructor(
)
}
private fun getRippleColorFallback(context: Context): ColorStateList {
return context.obtainStyledAttributes(intArrayOf(android.R.attr.colorControlHighlight)).use {
it.getColorStateList(0)
} ?: ColorStateList.valueOf(Color.TRANSPARENT)
private fun getRippleColor(context: Context): ColorStateList {
return ContextCompat.getColorStateList(context, R.color.selector_overlay)
?: ColorStateList.valueOf(Color.TRANSPARENT)
}
}
}

View File

@@ -0,0 +1,123 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.core.graphics.ColorUtils
import com.google.android.material.R as materialR
import kotlin.random.Random
import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.resolveDp
class SegmentedBarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val segmentsData = ArrayList<Segment>()
private val segmentsSizes = ArrayList<Float>()
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
private var cornerSize = 0f
var segments: List<Segment>
get() = segmentsData
set(value) {
segmentsData.replaceWith(value)
updateSizes()
invalidate()
}
init {
paint.strokeWidth = context.resources.resolveDp(1f)
outlineProvider = OutlineProvider()
clipToOutline = true
if (isInEditMode) {
segments = List(Random.nextInt(3, 5)) {
Segment(
percent = Random.nextFloat(),
color = ColorUtils.HSLToColor(floatArrayOf(Random.nextInt(0, 360).toFloat(), 0.5f, 0.5f)),
)
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cornerSize = h / 2f
updateSizes()
}
override fun onDraw(canvas: Canvas) {
if (segmentsSizes.isEmpty()) {
return
}
val w = width.toFloat()
var x = w - segmentsSizes.last()
for (i in (0 until segmentsData.size).reversed()) {
val segment = segmentsData[i]
paint.color = segment.color
paint.style = Paint.Style.FILL
val segmentWidth = segmentsSizes[i]
canvas.drawRoundRect(0f, 0f, x + cornerSize, height.toFloat(), cornerSize, cornerSize, paint)
paint.color = outlineColor
paint.style = Paint.Style.STROKE
canvas.drawRoundRect(0f, 0f, x + cornerSize, height.toFloat(), cornerSize, cornerSize, paint)
x -= segmentWidth
}
paint.color = outlineColor
paint.style = Paint.Style.STROKE
canvas.drawRoundRect(0f, 0f, w, height.toFloat(), cornerSize, cornerSize, paint)
}
private fun updateSizes() {
segmentsSizes.clear()
segmentsSizes.ensureCapacity(segmentsData.size + 1)
var w = width.toFloat()
for (segment in segmentsData) {
val segmentWidth = (w * segment.percent).coerceAtLeast(cornerSize)
segmentsSizes.add(segmentWidth)
w -= segmentWidth
}
segmentsSizes.add(w)
}
class Segment(
@FloatRange(from = 0.0, to = 1.0) val percent: Float,
@ColorInt val color: Int,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Segment
if (percent != other.percent) return false
if (color != other.color) return false
return true
}
override fun hashCode(): Int {
var result = percent.hashCode()
result = 31 * result + color
return result
}
}
private class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
}
}
}

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.text.Selection
import android.text.Spannable
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.annotation.AttrRes
import com.google.android.material.textview.MaterialTextView
class SelectableTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = android.R.attr.textViewStyle,
) : MaterialTextView(context, attrs, defStyleAttr) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
fixSelectionRange()
return super.dispatchTouchEvent(event)
}
// https://stackoverflow.com/questions/22810147/error-when-selecting-text-from-textview-java-lang-indexoutofboundsexception-se
private fun fixSelectionRange() {
if (selectionStart < 0 || selectionEnd < 0) {
val spannableText = text as? Spannable ?: return
Selection.setSelection(spannableText, text.length)
}
}
}

View File

@@ -0,0 +1,142 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.ViewPropertyAnimator
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.customview.view.AbsSavedState
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import com.google.android.material.R as materialR
import com.google.android.material.bottomnavigation.BottomNavigationView
import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale
import org.koitharu.kotatsu.utils.ext.measureHeight
private const val STATE_DOWN = 1
private const val STATE_UP = 2
private const val SLIDE_UP_ANIMATION_DURATION = 225L
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
class SlidingBottomNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
@StyleRes defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes),
CoordinatorLayout.AttachedBehavior {
private var currentAnimator: ViewPropertyAnimator? = null
private var currentState = STATE_UP
private var behavior = HideBottomNavigationOnScrollBehavior()
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
return behavior
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return SavedState(superState, currentState, translationY)
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
super.setTranslationY(state.translationY)
currentState = state.currentState
} else {
super.onRestoreInstanceState(state)
}
}
override fun setTranslationY(translationY: Float) {
// Disallow translation change when state down
if (currentState != STATE_DOWN) {
super.setTranslationY(translationY)
}
}
fun show() {
currentAnimator?.cancel()
clearAnimation()
currentState = STATE_UP
animateTranslation(
0F,
SLIDE_UP_ANIMATION_DURATION,
LinearOutSlowInInterpolator(),
)
}
fun hide() {
currentAnimator?.cancel()
clearAnimation()
currentState = STATE_DOWN
val target = measureHeight()
if (target == 0) {
return
}
animateTranslation(
target.toFloat(),
SLIDE_DOWN_ANIMATION_DURATION,
FastOutLinearInInterpolator(),
)
}
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
currentAnimator = animate()
.translationY(targetY)
.setInterpolator(interpolator)
.setDuration(duration)
.applySystemAnimatorScale(context)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
currentAnimator = null
postInvalidate()
}
},
)
}
internal class SavedState : AbsSavedState {
var currentState = STATE_UP
var translationY = 0F
constructor(superState: Parcelable, currentState: Int, translationY: Float) : super(superState) {
this.currentState = currentState
this.translationY = translationY
}
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
currentState = source.readInt()
translationY = source.readFloat()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(currentState)
out.writeFloat(translationY)
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(`in`: Parcel) = SavedState(`in`, SavedState::class.java.classLoader)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}

View File

@@ -1,14 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
class SquareLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
}
}

View File

@@ -0,0 +1,72 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.WindowInsets
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.WindowInsetsCompat
class WindowInsetHolder @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
private var desiredHeight = 0
private var desiredWidth = 0
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
val barsInsets = WindowInsetsCompat.toWindowInsetsCompat(insets, this)
.getInsets(WindowInsetsCompat.Type.systemBars())
val gravity = getLayoutGravity()
val newWidth = when (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
Gravity.LEFT -> barsInsets.left
Gravity.RIGHT -> barsInsets.right
else -> 0
}
val newHeight = when (gravity and Gravity.VERTICAL_GRAVITY_MASK) {
Gravity.TOP -> barsInsets.top
Gravity.BOTTOM -> barsInsets.bottom
else -> 0
}
if (newWidth != desiredWidth || newHeight != desiredHeight) {
desiredWidth = newWidth
desiredHeight = newHeight
requestLayout()
}
return super.onApplyWindowInsets(insets)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width: Int = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
private fun getLayoutGravity(): Int {
return when (val lp = layoutParams) {
is FrameLayout.LayoutParams -> lp.gravity
is LinearLayout.LayoutParams -> lp.gravity
is CoordinatorLayout.LayoutParams -> lp.gravity
else -> Gravity.NO_GRAVITY
}
}
}

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.bookmarks.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity(
tableName = "bookmarks",
primaryKeys = ["manga_id", "page_id"],
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class BookmarkEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "page_id", index = true) val pageId: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int,
@ColumnInfo(name = "scroll") val scroll: Int,
@ColumnInfo(name = "image") val imageUrl: String,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "percent") val percent: Float,
)

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.bookmarks.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao
abstract class BookmarksDao {
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
@Transaction
@Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
)
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
@Insert
abstract suspend fun insert(entity: BookmarkEntity)
@Delete
abstract suspend fun delete(entity: BookmarkEntity)
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun delete(mangaId: Long, pageId: Long)
}

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.bookmarks.data
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
manga = manga,
pageId = pageId,
chapterId = chapterId,
page = page,
scroll = scroll,
imageUrl = imageUrl,
createdAt = Date(createdAt),
percent = percent,
)
fun Bookmark.toEntity() = BookmarkEntity(
mangaId = manga.id,
pageId = pageId,
chapterId = chapterId,
page = page,
scroll = scroll,
imageUrl = imageUrl,
createdAt = createdAt.time,
percent = percent,
)
fun Collection<BookmarkEntity>.toBookmarks(manga: Manga) = map {
it.toBookmark(manga)
}
fun Collection<Bookmark>.ids() = map { it.pageId }

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
class Bookmark(
val manga: Manga,
val pageId: Long,
val chapterId: Long,
val page: Int,
val scroll: Int,
val imageUrl: String,
val createdAt: Date,
val percent: Float,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Bookmark
if (manga != other.manga) return false
if (pageId != other.pageId) return false
if (chapterId != other.chapterId) return false
if (page != other.page) return false
if (scroll != other.scroll) return false
if (imageUrl != other.imageUrl) return false
if (createdAt != other.createdAt) return false
if (percent != other.percent) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + pageId.hashCode()
result = 31 * result + chapterId.hashCode()
result = 31 * result + page
result = 31 * result + scroll
result = 31 * result + imageUrl.hashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + percent.hashCode()
return result
}
}

View File

@@ -0,0 +1,90 @@
package org.koitharu.kotatsu.bookmarks.domain
import android.database.SQLException
import androidx.room.withTransaction
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.toBookmark
import org.koitharu.kotatsu.bookmarks.data.toBookmarks
import org.koitharu.kotatsu.bookmarks.data.toEntity
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class BookmarksRepository @Inject constructor(
private val db: MangaDatabase,
) {
fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> {
return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
}
fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> {
return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) }
}
fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> {
return db.bookmarksDao.observe().map { map ->
val res = LinkedHashMap<Manga, List<Bookmark>>(map.size)
for ((k, v) in map) {
val manga = k.toManga()
res[manga] = v.toBookmarks(manga)
}
res
}
}
suspend fun addBookmark(bookmark: Bookmark) {
db.withTransaction {
val tags = bookmark.manga.tags.toEntities()
db.tagsDao.upsert(tags)
db.mangaDao.upsert(bookmark.manga.toEntity(), tags)
db.bookmarksDao.insert(bookmark.toEntity())
}
}
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
db.bookmarksDao.delete(mangaId, pageId)
}
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
val entities = ArrayList<BookmarkEntity>(ids.size)
db.withTransaction {
val dao = db.bookmarksDao
for ((manga, idSet) in ids) {
for (pageId in idSet) {
val e = dao.find(manga.id, pageId)
if (e != null) {
entities.add(e)
}
dao.delete(manga.id, pageId)
}
}
}
return BookmarksRestorer(entities)
}
private inner class BookmarksRestorer(
private val entities: Collection<BookmarkEntity>,
) : ReversibleHandle {
override suspend fun reverse() {
db.withTransaction {
for (e in entities) {
try {
db.bookmarksDao.insert(e)
} catch (e: SQLException) {
e.printStackTraceDebug()
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@AndroidEntryPoint
class BookmarksActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner,
SnackbarOwner {
override val appBar: AppBarLayout
get() = binding.appbar
override val snackbarHost: CoordinatorLayout
get() = binding.root
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
val fragment = BookmarksFragment.newInstance()
replace(R.id.container, fragment)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java)
}
}

View File

@@ -0,0 +1,200 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.os.Bundle
import android.view.*
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.data.ids
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
@AndroidEntryPoint
class BookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
SectionedSelectionController.Callback<Manga>,
FastScroller.FastScrollListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<BookmarksViewModel>()
private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
return FragmentListSimpleBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
selectionController = SectionedSelectionController(
activity = requireActivity(),
owner = this,
callback = this,
)
adapter = BookmarksGroupAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
listener = this,
selectionController = checkNotNull(selectionController),
bookmarkClickListener = this,
groupClickListener = OnGroupClickListener(),
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {
super.onDestroyView()
adapter = null
selectionController = null
}
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
val intent = ReaderActivity.newIntent(view.context, item)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
}
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
}
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = Unit
override fun onFastScrollStart(fastScroller: FastScroller) {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
}
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
binding.recyclerView.invalidateNestedItemDecorations()
}
override fun onCreateActionMode(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
menu: Menu,
): Boolean {
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
return true
}
override fun onActionItemClicked(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
item: MenuItem,
): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
val ids = selectionController?.snapshot() ?: return false
viewModel.removeBookmarks(ids)
mode.finish()
true
}
else -> false
}
}
override fun onCreateItemDecoration(
controller: SectionedSelectionController<Manga>,
section: Manga,
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
private fun onListChanged(list: List<ListModel>) {
adapter?.items = list
}
private fun onError(e: Throwable) {
Snackbar.make(
binding.recyclerView,
e.getDisplayMessage(resources),
Snackbar.LENGTH_SHORT,
).show()
}
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make((activity as SnackbarOwner).snackbarHost, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
override fun onItemClick(item: BookmarksGroup, view: View) {
val controller = selectionController
if (controller != null && controller.count > 0) {
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
controller.clearSelection(item.manga)
} else {
controller.addToSelection(item.manga, item.bookmarks.ids())
}
return
}
val intent = DetailsActivity.newIntent(view.context, item.manga)
startActivity(intent)
}
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
}
}
companion object {
fun newInstance() = BookmarksFragment()
}
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.utils.ext.getItem
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
override fun getItemId(parent: RecyclerView, child: View): Long {
val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
return item.pageId
}
}

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