Compare commits

...

469 Commits
v0.3 ... v2.1

Author SHA1 Message Date
677f71dd84 Increase version 2022-01-10 08:53:54 +02:00
Allan Nordhøy
3f90f88600 Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.9% (219 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-10 08:03:16 +02:00
229a7c70d9 Implement new manga sources settings list screen #78 2022-01-07 19:04:59 +02:00
a2dbec98f9 Fix Ninemanga filter 2022-01-06 17:46:46 +02:00
3a02f8090e Fix pages numbers 2022-01-06 17:31:49 +02:00
17519db44e Refactor sources settings list 2022-01-06 17:26:16 +02:00
99186bf269 Show pages numbers in reader 2022-01-06 09:58:20 +02:00
9a65e40be1 Update page flip animation 2022-01-05 15:46:22 +02:00
f0add59f99 Fix smooth cover loading in details 2022-01-05 15:04:14 +02:00
f18c182a6a Refactor filters 2022-01-05 14:49:18 +02:00
68e9588f24 Fix AniBel endpoint 2022-01-05 11:49:56 +02:00
eea427216d Fullscreen cover view activity 2022-01-05 11:42:03 +02:00
8e9b89f6f0 Fix GroupLe sources covers 2022-01-05 10:51:36 +02:00
4f3281be99 MangaDex source 2022-01-05 10:43:32 +02:00
badlop
eb56a82702 Translated using Weblate (Spanish)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-01-04 12:37:53 +02:00
nzgha
089ccc9d15 Translated using Weblate (Spanish)
Currently translated at 99.1% (244 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-01-04 12:37:53 +02:00
J. Lavoie
12c1365513 Translated using Weblate (Finnish)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2022-01-04 12:37:53 +02:00
J. Lavoie
7ecf9316e3 Translated using Weblate (French)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-04 12:37:53 +02:00
J. Lavoie
12e98ec36a Translated using Weblate (Italian)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-04 12:37:53 +02:00
J. Lavoie
22977fc7bc Translated using Weblate (German)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-04 12:37:53 +02:00
Aliaksiej Razumaŭ
b387a49a4e Translated using Weblate (Belarusian)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
dbbb0d0f64 Translated using Weblate (Norwegian Bokmål)
Currently translated at 88.2% (217 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
0bbf2b752f Translated using Weblate (English)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2022-01-04 12:37:53 +02:00
Aliaksiej Razumaŭ
14c1eacffa Translated using Weblate (Belarusian)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-01-04 12:37:53 +02:00
J. Lavoie
c2a0525bb8 Translated using Weblate (French)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-04 12:37:53 +02:00
J. Lavoie
4f502e580c Translated using Weblate (Italian)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-04 12:37:53 +02:00
J. Lavoie
7cb303966a Translated using Weblate (German)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
3f0431f88b Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.3% (214 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
aad5601df1 Translated using Weblate (English)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2022-01-04 12:37:53 +02:00
8f2cf8141a Mark HenChan manga as nsfw 2021-12-10 18:10:12 +02:00
eefd1129f7 Update dependencies, setup FragmentStrictMode 2021-12-04 12:09:13 +02:00
5ed0f8b5a6 Check if category name is not empty 2021-12-03 21:11:50 +02:00
9b4aa4fd64 Migrate AniBel parser to graphql 2021-12-03 20:30:20 +02:00
bbb226791b Merge branch 'hotifx/2.0.1' into devel 2021-11-22 08:48:11 +02:00
30ac4435d4 Update version 2021-11-22 08:41:35 +02:00
1b9dfe1901 Temporary change anibel domain 2021-11-21 17:41:32 +02:00
808a6efd8f [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:13:39 +02:00
66ed19ed5a [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:12:04 +02:00
527a3cbd09 Option to exclude NSFW content from history 2021-11-20 16:49:30 +02:00
f22963b315 Use DownloadManager for pages saving 2021-11-18 20:06:44 +02:00
2ce5cb524f Increase version 2021-11-18 18:43:30 +02:00
4cbc6392fb Update AdapterDelegates library 2021-11-17 19:23:08 +02:00
049f9fa625 Fix cover image in lists 2021-11-17 19:08:13 +02:00
c853fae820 Fix browser activity insets 2021-11-12 20:46:25 +02:00
dd1d84a4fe Remanga authorization support #73 2021-11-12 20:36:01 +02:00
1569aa5dd5 Cleanup data classes 2021-11-12 20:13:49 +02:00
51cd88eded Update dependencies 2021-11-12 20:13:48 +02:00
Aliaksiej Razumaŭ
bf386deef0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-11-12 20:13:26 +02:00
J. Lavoie
5c80cdee81 Translated using Weblate (Spanish)
Currently translated at 98.7% (241 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-11-12 20:13:26 +02:00
J. Lavoie
b29fbb37cd Translated using Weblate (Finnish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2021-11-12 20:13:26 +02:00
J. Lavoie
589831beef Translated using Weblate (French)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-11-12 20:13:26 +02:00
J. Lavoie
0f5d153543 Translated using Weblate (Italian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-11-12 20:13:26 +02:00
J. Lavoie
ab1c99d132 Translated using Weblate (German)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-11-12 20:13:26 +02:00
af4870c39c Downscale phone screenshots in metadata 2021-10-25 20:34:27 +03:00
9581b3da65 Update tablet screenshots in metadata 2021-10-25 20:31:08 +03:00
358a907b74 Update phone screenshots in metadata 2021-10-25 08:44:44 +03:00
cdb1d8fe12 Fix manga cover in downloads queue 2021-10-24 17:32:45 +03:00
5513382aea Fix pages bottomsheet scrollbars 2021-10-20 19:34:40 +03:00
a7a5c8978d Fix page image saving 2021-10-20 19:07:36 +03:00
b139d5fca5 Fix saved manga index parsing 2021-10-20 18:50:08 +03:00
977da5b1b4 Optimize chapters list 2021-10-20 18:05:12 +03:00
78f2a13761 Chapter date parse optimization 2021-10-20 08:10:54 +03:00
7ded7fd12a Merge branch 'devel' of github.com:nv95/Kotatsu into devel 2021-10-20 07:41:30 +03:00
e50f79a25e VersionId test 2021-10-20 07:40:49 +03:00
904fc572d0 Adjustments 2021-10-20 07:39:32 +03:00
c280af9a5b Fix crash when trying to download a chapter with mobile internet 2021-10-20 07:39:32 +03:00
af6df6dfa2 Minor fixes 2021-10-20 07:39:32 +03:00
2380d69b11 Add chapter description (date, scanlator) 2021-10-20 07:39:32 +03:00
ad76d6d414 Use custom Snackbar for DetailsActivity 2021-10-20 07:39:32 +03:00
abidin toumi
d911ee12f2 Added translation using Weblate (Arabic) 2021-10-17 16:36:32 +03:00
J. Lavoie
e6ce03b516 Translated using Weblate (Finnish)
Currently translated at 100.0% (242 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2021-10-17 16:36:32 +03:00
J. Lavoie
17d7deef2d Translated using Weblate (Spanish)
Currently translated at 98.7% (239 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-10-17 16:36:32 +03:00
c2222344a2 Fix pagination 2021-10-09 15:56:19 +03:00
0a8d677fe8 Search by author name 2021-10-09 15:44:57 +03:00
a4e1381238 Improve CoverImageView 2021-10-08 18:56:35 +03:00
Aliaksiej Razumaŭ
1b6837d406 Translated using Weblate (Belarusian)
Currently translated at 100.0% (242 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-10-08 18:01:44 +03:00
J. Lavoie
2adf8a139c Translated using Weblate (French)
Currently translated at 100.0% (242 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-10-08 18:01:44 +03:00
J. Lavoie
efe96a6e05 Translated using Weblate (Italian)
Currently translated at 100.0% (242 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-10-08 18:01:44 +03:00
J. Lavoie
0360df999f Translated using Weblate (German)
Currently translated at 100.0% (242 of 242 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-10-08 18:01:44 +03:00
5cb4758b38 Remove unused strings 2021-10-05 18:53:44 +03:00
fce9f543e1 Trust user-added CAs 2021-10-05 18:53:44 +03:00
9e6cb1837e Remove "Unknown" status label due confusion 2021-10-05 18:53:44 +03:00
J. Lavoie
f9e40e17c4 Translated using Weblate (Spanish)
Currently translated at 97.0% (233 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-10-04 16:38:35 +03:00
J. Lavoie
a09d71cb13 Translated using Weblate (Finnish)
Currently translated at 100.0% (240 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2021-10-04 16:38:35 +03:00
J. Lavoie
de53445ac5 Translated using Weblate (French)
Currently translated at 100.0% (240 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-10-04 16:38:35 +03:00
J. Lavoie
19fdd54dbd Translated using Weblate (Italian)
Currently translated at 100.0% (240 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-10-04 16:38:35 +03:00
J. Lavoie
1c644188cd Translated using Weblate (German)
Currently translated at 100.0% (240 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-10-04 16:38:35 +03:00
J. Lavoie
a50943ed01 Translated using Weblate (Russian)
Currently translated at 99.5% (239 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2021-10-04 16:38:35 +03:00
J. Lavoie
1888aba335 Translated using Weblate (Spanish)
Currently translated at 96.6% (232 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-10-04 16:38:35 +03:00
b390fd49ca Translated using Weblate (Belarusian)
Currently translated at 100.0% (240 of 240 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-10-04 16:38:35 +03:00
J. Lavoie
33f0eb9f38 Added translation using Weblate (Finnish) 2021-10-04 16:38:35 +03:00
J. Lavoie
db91458abc Translated using Weblate (Spanish)
Currently translated at 97.4% (230 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-10-04 16:38:35 +03:00
Aliaksiej Razumaŭ
ee2ed0159d Translated using Weblate (Belarusian)
Currently translated at 100.0% (236 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-10-04 16:38:35 +03:00
0bdc3e024e MangaOwl adjustments 2021-10-04 16:38:13 +03:00
4aab4e636d Fix display of the snackbar in details activity 2021-10-04 16:38:13 +03:00
af114d74df Add new source: MangaOwl 2021-10-04 16:38:13 +03:00
c5921f8a62 Minor interface adjustments 2021-10-04 16:38:13 +03:00
be0c8f2c96 Add manga status label to details screen 2021-10-04 16:38:13 +03:00
bb685751cd Change Readmanga domain + set User-Agent 2021-10-04 16:38:13 +03:00
fd9737aa9c Update version 2021-09-26 17:20:43 +03:00
1dcb479d62 Add transition to search view 2021-09-23 19:36:01 +03:00
7618a05162 Fix inability to create a backup 2021-09-19 13:57:35 +03:00
174c6649e0 Use more correct detection of dark AMOLED theme 2021-09-19 13:57:35 +03:00
203608e9fd Minor fix download item 2021-09-19 13:57:35 +03:00
07e0ae884c Fix color when search opened with AMOLED mode 2021-09-19 13:57:35 +03:00
cd8e256364 Fixed the toolbar color with a light theme and AMOLED mode enabled 2021-09-19 13:57:35 +03:00
93998e460c Fix chopped shadows on thumbnails 2021-09-19 13:57:35 +03:00
71f205ca8b Fix AMOLED theme 2021-09-19 13:57:35 +03:00
f9cee7a8f5 Update gradle and dependencies 2021-09-13 08:36:05 +03:00
675e95da2b Show current filter in list header 2021-09-11 16:01:15 +03:00
c1b6cef362 Multiple tags support in (almost) all sources #19 2021-09-11 13:54:56 +03:00
4977464e69 ExHentai manga source 2021-09-11 11:40:37 +03:00
593624fdb9 Manga repository authorization support 2021-09-08 07:27:25 +03:00
c4585c81e1 Merge branch 'devel' into feature/multitag 2021-09-05 16:16:17 +03:00
27293f1bf8 Remove some findViewById 2021-09-05 16:00:15 +03:00
d1fd31701d Use measured height instead of magic numbers 2021-09-05 15:48:52 +03:00
d30c7e6e9c Made UI more like Google apps 2021-09-05 15:48:52 +03:00
0355b61e69 Base support for multiple tags in repositories 2021-09-05 15:48:12 +03:00
XeroOl
5d5ec719b7 Update MangareadRepository.kt 2021-09-01 19:53:03 +03:00
6596dca291 Fix cbz cover fetcher closing, update Kotlin and other small fixes 2021-08-24 18:04:02 +03:00
a296c98602 Update dependencies 2021-08-22 12:31:02 +03:00
be0718acf4 Fix app crash on the first use 2021-08-22 12:17:25 +03:00
c42d913d4c Translated using Weblate (Belarusian)
Currently translated at 100.0% (236 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-08-18 08:11:14 +03:00
J. Lavoie
446649b2bb Translated using Weblate (French)
Currently translated at 100.0% (236 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-08-18 08:11:14 +03:00
J. Lavoie
9f145557ea Translated using Weblate (Italian)
Currently translated at 100.0% (236 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-08-18 08:11:14 +03:00
J. Lavoie
ae856fca74 Translated using Weblate (German)
Currently translated at 100.0% (236 of 236 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-08-18 08:11:14 +03:00
6dc8a4ffb5 Use some animations in sheet toolbar 2021-08-18 08:10:59 +03:00
73498964a8 View close icon if thumbnails sheet is expanded 2021-08-18 08:10:59 +03:00
256f88cc60 Forgot to remove the layout behavior again, wow 2021-08-18 08:10:59 +03:00
16cc6fb117 Set horizontal margin to 32dp 2021-08-18 08:10:59 +03:00
7bb809f227 Improve thumbnail sheet 2021-08-18 08:10:59 +03:00
57111f628d Minor tweaks 2021-08-18 08:10:59 +03:00
0129e9e092 Merge branch 'weblate-kotatsu-strings' of https://github.com/weblate/Kotatsu into weblate-weblate-kotatsu-strings 2021-08-04 08:50:48 +03:00
d6c6132a04 Fixes 2021-08-04 08:46:23 +03:00
eb5976a796 Some changes in about section, fix links 2021-08-04 08:46:23 +03:00
253f4abba1 Minor fix 2021-08-04 08:46:23 +03:00
3a442817ce Add about section to settings, add some info stuff 2021-08-04 08:46:23 +03:00
594c359f1c Added information about the app to a separate activity 2021-08-04 08:46:23 +03:00
cc28d4fe54 Translated using Weblate (Russian)
Currently translated at 99.5% (224 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2021-08-04 07:41:41 +02:00
95708367a1 Translated using Weblate (Belarusian)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-08-04 07:41:41 +02:00
J. Lavoie
89b915b206 Translated using Weblate (French)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-08-04 07:41:41 +02:00
J. Lavoie
e4da0a126c Translated using Weblate (Italian)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-08-04 07:41:41 +02:00
J. Lavoie
56f9cc2c88 Translated using Weblate (German)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-08-04 07:41:41 +02:00
6037c66a2d Translated using Weblate (Russian)
Currently translated at 99.5% (224 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2021-08-04 07:41:41 +02:00
d25837b40b Merge branch 'devel' of https://github.com/nv95/Kotatsu into devel 2021-08-04 08:41:27 +03:00
fbd0f25b8f #8 Configure sort order for each favourites category 2021-08-04 08:40:32 +03:00
Allan Nordhøy
9c55fd166e Translated using Weblate (Norwegian Bokmål)
Currently translated at 86.6% (195 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2021-07-28 08:20:18 +03:00
Allan Nordhøy
2ac1828a0c Translated using Weblate (English)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2021-07-28 08:20:18 +03:00
J. Lavoie
45e1502c9b Translated using Weblate (French)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-07-28 08:20:18 +03:00
J. Lavoie
e2608cf85a Translated using Weblate (Italian)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-07-28 08:20:18 +03:00
J. Lavoie
05bbfe77b2 Translated using Weblate (German)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-07-28 08:20:18 +03:00
34ad0a7c68 Translated using Weblate (Russian)
Currently translated at 99.5% (224 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2021-07-28 08:20:18 +03:00
J. Lavoie
c67ce38350 Translated using Weblate (Spanish)
Currently translated at 93.3% (210 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-07-28 08:20:18 +03:00
ad79ff2739 Translated using Weblate (Belarusian)
Currently translated at 100.0% (225 of 225 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-07-28 08:20:18 +03:00
J. Lavoie
2e5afc73e7 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-07-28 08:20:18 +03:00
J. Lavoie
73efe6fd83 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-07-28 08:20:18 +03:00
HelaBasa
c59e3165b6 Translated using Weblate (Sinhala)
Currently translated at 4.0% (9 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/si/
2021-07-28 08:20:18 +03:00
J. Lavoie
ec8c5e0fd4 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-07-28 08:20:18 +03:00
J. Lavoie
149ac9280c Translated using Weblate (Italian)
Currently translated at 90.9% (201 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-07-28 08:20:18 +03:00
J. Lavoie
af20f65468 Translated using Weblate (German)
Currently translated at 88.6% (196 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-07-28 08:20:18 +03:00
6f7efa9e26 Optimize layout 2021-07-28 08:03:16 +03:00
7f5ef227eb Show not downloaded chapters in local manga 2021-07-24 10:51:26 +03:00
e8e95a485b Downloads queue activity 2021-07-23 06:51:01 +03:00
77186d271d Fix list headers 2021-07-21 19:13:30 +03:00
ebeaf9703f Refactor download service 2021-07-21 19:13:29 +03:00
625b2769c6 Improve search ui 2021-07-21 19:13:29 +03:00
52e136ddef Source name in list header 2021-07-21 19:13:29 +03:00
78fe18735b Database migrations test 2021-07-21 19:13:29 +03:00
HelaBasa
2f89c0bb92 Added translation using Weblate (Sinhala) 2021-07-21 07:02:50 +03:00
J. Lavoie
fbac8881ce Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-07-21 07:02:50 +03:00
J. Lavoie
b51b3460c0 Translated using Weblate (Italian)
Currently translated at 31.6% (70 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-07-21 07:02:50 +03:00
J. Lavoie
aaea4147a4 Translated using Weblate (German)
Currently translated at 37.1% (82 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-07-21 07:02:50 +03:00
d2609c0560 Improve remote repository tests 2021-07-16 07:43:20 +03:00
6a3421df8a Adjust nullability in parsers 2021-07-14 07:01:11 +03:00
86be393335 Update dependencies 2021-07-14 07:00:41 +03:00
Allan Nordhøy
96d6f9d80d Translated using Weblate (Norwegian Bokmål)
Currently translated at 86.8% (192 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2021-07-12 20:45:18 +03:00
384d0345f5 Translated using Weblate (Belarusian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-07-12 20:45:18 +03:00
eb780a1449 Added translation using Weblate (French) 2021-07-12 20:45:18 +03:00
15d094a175 Added translation using Weblate (Portuguese) 2021-07-12 20:45:18 +03:00
eba5e484d6 Added translation using Weblate (Italian) 2021-07-12 20:45:18 +03:00
7402e8569a Added translation using Weblate (German) 2021-07-12 20:45:18 +03:00
Allan Nordhøy
8ae7863185 Added translation using Weblate (Norwegian Bokmål) 2021-07-12 20:45:18 +03:00
Hosted Weblate
75b9fd1b7a Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
2021-07-12 20:45:18 +03:00
Hosted Weblate
dc46657fa6 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
2021-07-12 20:45:18 +03:00
d77177bbfd Change some colors of dark theme 2021-07-12 20:44:44 +03:00
7c6a97e264 Using Totoro vector icon 2021-07-12 20:44:44 +03:00
23f84c2416 Add Weblate badge to README 2021-07-12 20:43:40 +03:00
d41a813e41 Merge branch 'master' into devel 2021-07-12 08:48:34 +03:00
fae958f6ef Remove unused resources 2021-07-07 07:25:31 +03:00
d8db89326f Info about background restrictions 2021-07-07 07:13:02 +03:00
3804896788 Save backup instead of sharing 2021-07-06 20:26:52 +03:00
4aedea7e15 Improve accesibility in reader 2021-07-06 19:20:41 +03:00
ed89d76488 Fix resource shrinking 2021-07-03 16:12:16 +03:00
bbd43b51e3 Increase version 2021-06-28 15:46:13 +03:00
e5448fa8ab Add some widgets 2021-06-28 15:41:39 +03:00
a6a392c7bf Merge pull request #36 from ztimms73/fix-crashes
Fix possible crashes
2021-06-28 15:41:14 +03:00
08f92f9614 Fix possible crashes 2021-06-28 13:59:45 +03:00
c9cf09f4dd Merge pull request #34 from ztimms73/ui-patch
Some UI changes
2021-06-26 16:17:36 +03:00
ab1624c918 Replace ProgressBar with ProgressIndicator 2021-06-26 16:15:21 +03:00
62396111e3 Update dependencies 2021-06-26 15:51:57 +03:00
e37f6f31da Merge branch 'master' into devel 2021-06-26 15:19:50 +03:00
873b41e4f9 Fix default languages selector 2021-06-26 15:15:53 +03:00
f0d4deffd7 Fix Mangaread parser 2021-06-26 14:49:13 +03:00
b6c50d59ed #35 Fix Mangaread parser 2021-06-24 07:27:07 +03:00
9fcc19ef7e Fix filename transliteration 2021-06-23 19:34:10 +03:00
b90ebdabf9 Fix storage dirs enumeration 2021-06-21 17:53:04 +03:00
e08a4cf1b2 Fix crash if title is empty 2021-06-21 17:50:16 +03:00
bd4efcf110 Remove old search option from menu 2021-06-21 17:32:23 +03:00
0b9013e8b2 Use expanded SearchView in Toolbar instead of an menu option 2021-06-21 14:06:08 +03:00
445128f462 Forgot set correct icon 2021-06-20 20:13:19 +03:00
f50a8b3112 Fix toolbars not scrolled when option was enabled 2021-06-20 20:11:17 +03:00
ed65145f83 Change some icons for empty views 2021-06-20 20:07:44 +03:00
49e08eaf2f UI changes 2021-06-20 19:30:29 +03:00
83a9570961 Prompt before search history clear 2021-06-20 16:04:12 +03:00
973a4073f0 Fix crash in history settings 2021-06-20 15:59:24 +03:00
867812b8e3 Update koin 2021-06-20 15:17:48 +03:00
cf7341b065 Update translations 2021-06-20 14:55:22 +03:00
de3c4545e6 Fix statusbar color for actionmode 2021-06-18 07:47:36 +03:00
a483d21120 Merge branch 'redesign' of https://github.com/ztimms73/Kotatsu into devel 2021-06-18 07:35:24 +03:00
665d46b7c4 Some reformat 2021-06-18 03:20:41 +03:00
3d5a1e9b30 Finally update details layouts 2021-06-18 03:17:38 +03:00
e7e9c5fe9f Avoiding overlapping content 2021-06-17 23:35:19 +03:00
c71460fcd8 Minor fixes 2021-06-17 23:33:52 +03:00
8c2bc078e5 Manga languages onboarding 2021-06-17 19:45:36 +03:00
75b1068d46 Little cleanup 2021-06-17 10:06:52 +03:00
4ac406aa2d Reformat and some fixes 2021-06-17 10:03:36 +03:00
f4f367850e Cleanup unused resources 2021-06-17 09:54:28 +03:00
b293e9f370 Minor fixes 2021-06-17 01:57:52 +03:00
4e2350e5fc Two lines of title are better than one 2021-06-17 01:41:31 +03:00
09412719b7 Redesign (#24) 2021-06-17 00:46:27 +03:00
cd7d6d7674 New search suggestion UI 2021-06-03 19:15:57 +03:00
bc0c5ac71a Fix Anibel titles 2021-05-23 17:17:08 +03:00
91619cc259 NineManga sources #14 2021-05-23 16:58:45 +03:00
f0c9c61b49 Fix issues with spanish translation 2021-05-20 08:12:09 +03:00
d65158b7b9 Update Kotlin to 1.5 and dependencies 2021-05-20 08:09:35 +03:00
4e5de1e33e Merge pull request #32 from ztimms73/devel
Fix some problems from issue #23
2021-05-19 06:51:49 +03:00
b009a6423d Fix some problems from issue #23 2021-05-18 11:17:00 +03:00
467d0c8e18 Merge pull request #30 from ztimms73/sources
Fix Remanga sort, new manga source Anibel, UI changes
2021-05-17 19:48:52 +03:00
4d535cef41 Avoid searching for anime on Anibel 2021-05-15 22:51:45 +03:00
98147d0a81 Fix Anibel issues 2021-05-15 22:05:32 +03:00
323c1defaa Fix Remanga (closes #28) 2021-05-15 10:28:23 +03:00
Zakhar Timoshenko
60c5408ae8 Merge branch 'nv95:devel' into devel 2021-05-15 10:11:07 +03:00
51dc2ac046 Added the option to hide or not toolbar when scrolling 2021-05-15 08:56:03 +03:00
24d9a49420 Merge remote-tracking branch 'origin/devel' into devel 2021-05-14 22:45:07 +03:00
46891aa958 Minor UI changes 2021-05-14 22:43:14 +03:00
d1921193f0 Add Anibel source 2021-05-14 22:42:43 +03:00
a8c4c4045c Merge pull request #27 from ztimms73/devel
Belarusian translation and minor fixes in Russian
2021-05-14 07:16:06 +03:00
Zakhar Timoshenko
0559c13dc6 Fix 2021-05-13 20:29:01 +03:00
Xtimms
3fbec046ba Belarusian translation and minor fixes in Russian 2021-05-13 20:26:17 +03:00
4c8fa91af4 Merge pull request #26 from sguinetti/devel
Add spanish translation
2021-05-12 07:05:07 +03:00
sguinetti
1568c09fa2 Add spanish translation 2021-05-09 11:21:03 -05:00
0e74d6e017 Merge branch 'master' into devel 2021-05-03 18:18:55 +03:00
7690a29efb Add f-droid link to readme 2021-05-03 18:16:33 +03:00
b1d6f5debd Fix onApplyWindowInsets infinite call 2021-04-29 08:06:37 +03:00
fbb92005a1 Update database structure 2021-04-29 07:57:43 +03:00
5b9922d509 Remove unused code 2021-04-29 07:24:26 +03:00
49eebdf554 Specify LazyThreadSafetyMode for inject and viewModel delegates 2021-04-27 19:52:20 +03:00
2da941d550 Update dependencies 2021-04-27 19:41:41 +03:00
5a8d7531bf Ui fixes 2021-04-11 12:20:12 +03:00
4d1f5e22d3 #21 Fix cloudflare passing 2021-04-11 11:33:04 +03:00
012416c881 Improve password protection 2021-04-11 11:24:50 +03:00
0f48ad07a3 Support no-fullheight pages in webtoon mode 2021-04-04 11:18:12 +03:00
64752da948 Optimize global search 2021-04-02 15:26:27 +03:00
95148a1071 Add distributionSha256Sum for gradle wrapper 2021-03-30 07:49:10 +03:00
d9d0656ef4 Update dependencies and gradle 2021-03-29 20:00:05 +03:00
b17d8efa5c Update Koin to 3.0 2021-03-29 19:18:08 +03:00
b07fcf5842 Use static version code 2021-03-28 11:34:57 +03:00
f669a1ca0f Update version name 2021-03-25 19:38:50 +02:00
5a921ea862 Update readme 2021-03-25 19:30:28 +02:00
810efde0b0 Add metadata 2021-03-25 19:17:46 +02:00
03510a1f19 Update dependencies 2021-03-25 07:47:43 +02:00
049f32d2f0 Fix smooth page switching 2021-03-21 18:33:13 +02:00
11a9db3cc2 Fix "Unsupported image format" error 2021-03-21 18:18:57 +02:00
57dd5743f0 Fix tabs on landscape 2021-03-21 18:16:38 +02:00
5f37e76c85 Update dialogs 2021-03-19 21:11:36 +02:00
fc51d49505 Fix lists padding 2021-03-19 20:42:14 +02:00
3dde254452 Refresh tabs style 2021-03-19 20:14:08 +02:00
aa21dd9721 Fixes 2021-03-19 19:38:29 +02:00
71f5ee8cb1 Support for multiple manga branches (translations, etc) 2021-03-08 10:30:15 +02:00
40f27ae634 Fix page saving 2021-02-25 20:10:38 +02:00
0dfba47d85 Misc small fixes 2021-02-19 20:21:44 +02:00
69e44b10e9 Fix tags on details screen 2021-02-18 20:25:01 +02:00
4cd0cb04a3 Fix reader state restoration 2021-02-16 20:22:18 +02:00
d9d5595bde Ellipsize chapters in feed 2021-02-16 20:03:25 +02:00
ed70ca4e18 Information toast in reader 2021-02-11 19:29:17 +02:00
a371bb6514 Fix activity leak on Android Q 2021-02-08 20:29:32 +02:00
ee26a3e434 Fix search pagination 2021-02-03 07:14:46 +02:00
5bb6eae673 Use publicUrl instead of url 2021-02-03 07:09:29 +02:00
3357c00578 Add "public url" field to manga 2021-02-02 08:01:46 +02:00
1f2f40f077 Fix DesuMe parser 2021-02-01 20:37:41 +02:00
c25ee93ccb Update source preferences 2021-02-01 20:19:46 +02:00
4aa1b58109 Use relative urls for mangas and change id generation algorythm 2021-01-30 18:57:11 +02:00
c64115a268 Remove REQUEST_IGNORE_BATTERY_OPTIMIZATIONS feature 2021-01-29 07:20:10 +02:00
33296217a4 Fix title in BrowserActivity 2021-01-28 20:33:20 +02:00
0e384c134d Fix some manga sources 2021-01-27 18:24:48 +02:00
7f37c1f99e Option to reverse chapters order 2021-01-25 19:09:01 +02:00
d1aa0f0407 Fix action mode 2021-01-24 17:39:14 +02:00
4d904fe12f Tracker become foreground mode 2021-01-24 10:28:15 +02:00
3df8b8d170 Update preferences 2021-01-24 10:02:17 +02:00
42f0fa9bbf Fix grid spacing 2021-01-24 09:02:17 +02:00
5cbc592d23 Show manga source in details 2021-01-24 08:53:04 +02:00
85c424580a Add Remanga source 2021-01-23 19:44:11 +02:00
0d0e3acd04 Fix MangaLib search 2021-01-22 20:12:33 +02:00
49f9fb0488 Hide unknown rating in list 2021-01-22 19:41:19 +02:00
bbcd96b981 Clear updates feed from options menu 2021-01-22 19:17:49 +02:00
510c5b70c9 Show progress of new chapters checking 2021-01-22 18:26:02 +02:00
951a0db3f2 Fix snackbars in settings 2021-01-22 18:25:29 +02:00
d85f23b320 Replace RequestDraft with plain Url 2021-01-21 08:03:48 +02:00
0c0214a85e Fix MangaLib 2021-01-21 07:32:50 +02:00
9054f5720f Refactor repository creation 2021-01-20 20:27:47 +02:00
bb1dd74277 Support custom headers for page requests 2021-01-20 19:53:11 +02:00
96d437b2a8 Use system CookieManager as CookieJar 2021-01-20 07:50:35 +02:00
8f8d85d172 Fixes and refactor 2021-01-18 19:32:13 +02:00
a242aa6633 Fix backup restore 2021-01-10 11:13:39 +02:00
1a0986212b Support Referer header for image requests 2021-01-07 14:48:35 +02:00
22e7bab879 Fix chapters preloading 2021-01-07 13:18:57 +02:00
9bd7daef65 Shared RecycledViewPool 2021-01-07 10:41:50 +02:00
d1e17c8ec2 Fix favourites categories 2021-01-06 07:57:26 +02:00
e674e0f36f Optimize LiveData from flows 2020-12-28 07:20:21 +02:00
7fd71c13f3 Fixes 2020-12-26 16:23:13 +02:00
9a0b7c4700 Fix tracker duplicates 2020-12-20 17:44:05 +02:00
c54d128c09 Fix page saving 2020-12-20 17:40:25 +02:00
a1545fd889 Fix zoom changing 2020-12-20 16:49:51 +02:00
6e1fdcb19a Fix AlertDialogFragment view lifecycle 2020-12-20 16:37:27 +02:00
72bedfd92e Fix reader animation changes 2020-12-18 07:08:18 +02:00
c132f1d5c4 Fix shortcuts 2020-12-17 19:07:22 +02:00
abc4ab92a9 Fix reader immersive mode 2020-12-17 18:22:22 +02:00
0931e4e0e6 Misc fixes 2020-12-16 17:48:26 +02:00
113cde2f07 Sort favourites by date descending 2020-12-16 15:30:54 +02:00
bf2d82723b Refactor objects to classes 2020-12-16 15:28:58 +02:00
6463023736 Edge-to-edge ui 2020-12-16 13:24:49 +02:00
b8d2fa69c4 Change shortcuts target to reader 2020-12-16 08:32:57 +02:00
904d12f611 Refactor reader 2020-12-16 08:26:01 +02:00
71a5801a0c Some fixes for favourites 2020-12-08 07:26:52 +02:00
6b529f806f Fix default value for some preferences 2020-12-05 18:15:18 +02:00
9b5510ac59 Move list states to adapter delegates 2020-12-05 18:03:34 +02:00
90be936c82 Optimize images loading 2020-12-01 19:49:04 +02:00
29e6eab0e7 Drop Jetifier 2020-12-01 18:30:16 +02:00
75b3ea0bc9 Migrate to ViewBinding 2020-12-01 18:30:11 +02:00
a215d9ebfc Fix search 2020-11-28 17:57:12 +02:00
cef5d91eec Fast scroll in lists 2020-11-28 14:15:31 +02:00
9c20559962 Option to group history by date 2020-11-28 14:02:03 +02:00
b1be45af8b Merge branch 'feature/mvvm' into devel 2020-11-28 11:05:59 +02:00
5ed4d0b6b7 Fully migrate to AdapterDelegates and cleanup code 2020-11-28 11:05:27 +02:00
53e36d23b1 Migrate favourite categories to AdapterDelegates 2020-11-28 08:30:49 +02:00
fa02cfd7e8 Migrate details to AdapterDelegates and mvvm 2020-11-24 07:37:23 +02:00
1b1540b35b Upgrade kotlin 2020-11-23 19:28:16 +02:00
b9f35f34ad Migrate feed to adapter delegates 2020-11-23 19:16:02 +02:00
12c8cdfd70 Remove Related manga tab from details 2020-11-20 20:15:50 +02:00
971f708e45 Fully manga list fragments to AdapterDelegates and mvvm 2020-11-20 20:07:57 +02:00
7e76e10591 Migrate to AdapterDelegates 2020-11-19 20:43:36 +02:00
7d24286c55 Migrate to MVVM 2020-11-18 06:56:12 +02:00
eaac271143 Remove from favourites via popup menu 2020-11-17 07:32:44 +02:00
d135898b49 Backup and restore user data 2020-11-16 19:16:31 +02:00
03dbd86363 Fix some StrictMode warnings 2020-11-09 20:33:53 +02:00
908baebb62 Dark amoled theme 2020-11-09 19:43:01 +02:00
5190ec3e98 Passing CloudFlare checks 2020-11-09 19:17:08 +02:00
17c20b2bf9 Handle CloudFlare protection #13 2020-11-07 17:50:08 +02:00
e2b65f6fb6 Fix Grouple external links 2020-11-07 17:24:11 +02:00
28a9659410 Scale mode option for reader 2020-11-07 15:51:07 +02:00
bdebd0578e Fix crash in settings 2020-11-03 18:06:09 +02:00
53542f3f86 Update crash activity 2020-11-03 17:39:23 +02:00
2772f0b3dd Remove preferences keys from resources 2020-11-03 16:51:31 +02:00
95a4bf41d2 Popup menu on favourite tabs 2020-11-02 19:54:43 +02:00
e497781359 Option to prefer Rtl reader 2020-11-02 19:13:07 +02:00
72fdc7796f Use ArrayDeque in reader 2020-10-28 07:56:16 +02:00
a885709ba9 Reversed reader mode #15 2020-10-25 11:55:15 +02:00
578c1c3825 Ui fixes 2020-10-23 20:44:55 +03:00
a5fba83510 Refactor: Provide manga parsers via DI 2020-10-20 21:45:24 +03:00
6f3ae19345 Checking for updates in settings 2020-10-20 20:52:43 +03:00
2135195f27 MangaRead parser #17 2020-10-19 20:24:58 +03:00
a8c22de601 Reduce memory usage 2020-10-18 20:11:08 +03:00
56e145420c DI refactoring 2020-10-18 19:05:15 +03:00
fb60b26f08 Remove chucker 2020-10-18 15:14:14 +03:00
ff3ebbf1d9 Add NudeMoon parser (broken) 2020-10-18 15:09:42 +03:00
4dc9df0515 Refactor 2020-10-11 17:11:34 +03:00
e9bce8ef15 Password protection 2020-10-11 16:45:29 +03:00
55fc1aeadd Option to configure tracked manga 2020-10-11 13:55:04 +03:00
693f568b8e Refactor BasePresenter 2020-10-11 13:16:38 +03:00
5293a8d209 Update dependencies 2020-10-11 12:50:52 +03:00
1c46fc7f23 Fix manga downloading 2020-09-30 20:36:53 +03:00
b7e4c6b8c0 New cbz write utility 2020-09-26 17:51:47 +03:00
df599e9d50 Fix MangaLib parser 2020-09-25 19:44:18 +03:00
6009f089e7 Update coil version 2020-09-25 19:23:57 +03:00
0a4f2f848e UI fixes 2020-09-20 17:31:33 +03:00
85fc3a024c Fix henchan 2020-09-20 17:08:42 +03:00
eeb536b1ac Fix crash on import 2020-09-19 17:32:22 +03:00
5b8e8d76c0 Update target sdk 2020-09-19 17:27:45 +03:00
73cf2964b2 Option to manually track manga updates 2020-09-19 15:22:18 +03:00
8372f9b5de Update dependencies 2020-09-19 14:21:32 +03:00
d7181e35e7 Update dependencies 2020-09-05 19:37:40 +03:00
55d824bb94 Remove header from drawer 2020-08-31 19:40:58 +03:00
229d9fa2ae Update launcher icon 2020-08-31 19:37:14 +03:00
3eb68e1ff9 Manual screen rotation in reader 2020-08-28 21:01:59 +03:00
b103589bba Fix unsupported image formats in reader 2020-08-27 19:54:53 +03:00
0726c037a4 Update dependencies 2020-08-26 16:14:32 +03:00
0ff64931e0 Close drawer on back pressed 2020-08-09 16:54:00 +03:00
2374c96009 Fix source preferences summaries 2020-07-19 11:44:12 +03:00
2dd51117e9 Remove unused resources 2020-07-16 19:27:59 +03:00
6c5f3c7d97 Fix readmanga search 2020-07-16 19:20:59 +03:00
626bb20edb Fix global search 2020-07-12 13:48:52 +03:00
d363869dab Fix cbz thumbnails 2020-07-10 07:22:16 +03:00
774f33c63d Get remote manga for local in tracker 2020-07-10 07:14:41 +03:00
079427346a Update dependencies 2020-07-10 07:09:41 +03:00
a1a3125834 Fix crash on manga downloading 2020-07-06 20:05:41 +03:00
fc9c8f8a79 Update readme 2020-07-05 17:30:44 +03:00
c06923dbdf Merge branch 'devel' 2020-07-05 17:14:37 +03:00
66ca51cc73 Fix MangaTown endless search 2020-07-05 17:09:38 +03:00
bf45480366 Update henchan default domain 2020-07-05 17:01:43 +03:00
28618e394e Fix *chan search 2020-07-05 16:59:20 +03:00
9762a466ce Fix Mangalib pages loading 2020-07-05 16:45:34 +03:00
367a97a95b Fix page error processing 2020-07-01 19:37:15 +03:00
c3ab197aa0 Fix some StrictMode warnings 2020-07-01 19:17:08 +03:00
a0aa33a499 Option to clear updates feed 2020-06-29 13:43:01 +03:00
b27bc86141 Fix search history preference 2020-06-29 13:28:25 +03:00
84ef2af82f Fix MangaDetailsPresenter sharing 2020-06-29 13:26:28 +03:00
a2f09d8763 Restore download after network error 2020-06-29 09:48:17 +03:00
79058440a1 Fix grid span count #11 2020-06-25 19:40:50 +03:00
7f9cfdbf7a Show app update in dialog 2020-06-20 12:10:42 +03:00
85f7477450 Cleaning up traks 2020-06-20 11:35:42 +03:00
0e08d75626 Update dependencies 2020-06-14 17:12:30 +03:00
1b4a65f476 Update pages thumbnails list 2020-06-11 19:59:54 +03:00
2e69395ade Update ui 2020-06-08 19:22:56 +03:00
3f61f13b7b Show related manga 2020-06-07 20:14:44 +03:00
10a0f0ad53 Fix empty tracker log records 2020-06-05 19:25:10 +03:00
680fc66f21 Tracker fixes 2020-06-03 18:52:00 +03:00
e01b74ee3d Pagination loading indicator 2020-05-31 12:10:43 +03:00
3539e6a892 Global search 2020-05-30 11:18:14 +03:00
ff56f5a343 Fix updates feed 2020-05-30 10:25:48 +03:00
9ce43a39c8 Global search 2020-05-30 09:48:04 +03:00
0e3aa3f380 Update readme 2020-05-24 13:25:38 +03:00
7927bf0c9a Refactor 2020-05-24 12:58:17 +03:00
aec2d71688 Manga updates feed 2020-05-22 20:28:14 +03:00
140a0f4d66 Log manga tracking 2020-05-22 19:20:08 +03:00
7cf57535ab Update dependencies 2020-05-21 21:27:21 +03:00
31fe924157 Merge branch 'master' into devel 2020-05-21 21:20:39 +03:00
6d193baa69 Increment version 2020-05-21 20:48:01 +03:00
3bd7b54405 Capitalize MangaLib genres 2020-05-21 20:47:10 +03:00
d99450c5a3 Prepopulate favourite categories 2020-05-21 20:43:24 +03:00
6444122c0a Update database: add tracklogs table 2020-05-21 20:30:10 +03:00
fe14ccb5ec Update readme 2020-05-20 20:05:51 +03:00
e38e5fdf0f Small enhancements 2020-05-20 19:51:28 +03:00
c1c2b11bd8 Show local manga size 2020-05-20 19:31:17 +03:00
7d147b3c37 Check wakelock is held in download service 2020-05-20 19:04:31 +03:00
260ff32cd1 Detect chapters in cbz if index missing 2020-05-20 18:59:30 +03:00
ccc5f3e423 Fix webtoon scroll 2020-05-17 17:25:18 +03:00
8b32a60743 Misc fixes 2020-05-16 10:20:55 +03:00
c1faf2fe06 Remember last opened section 2020-05-16 09:07:09 +03:00
3588270742 Show favourites by categories and manager categories order 2020-05-16 08:49:34 +03:00
01607ec1e2 Fix MangaLib provider 2020-05-13 20:17:37 +03:00
50f8cb9193 Action mode chapters selection 2020-05-11 17:30:59 +03:00
0100974508 Optimize webtoon scroll 2020-05-11 14:04:51 +03:00
b438898456 Add mangalib source 2020-05-10 18:52:00 +03:00
c3c43dce3d Update dependencies and fix deprecations 2020-05-10 13:38:14 +03:00
e33dfd63e4 Downgrade kotlin 2020-05-10 12:36:14 +03:00
1927500f5a Configure shelf appwidget 2020-05-09 14:15:13 +03:00
f9ccd0851d Small refactor 2020-05-09 12:33:09 +03:00
23412e5c17 Optimize webtoon scroll 2020-05-09 10:16:46 +03:00
1b7c8355ec Refactor: fix deprecations 2020-05-03 18:04:26 +03:00
8378b3dd90 Disable save action for local chapters 2020-05-03 17:31:33 +03:00
9ff5bb6352 Update readme 2020-04-26 20:43:26 +03:00
b2bb1d22df Fix default storage 2020-04-26 20:28:15 +03:00
34acf5bb55 Fix json null string 2020-04-26 20:25:46 +03:00
5af32898f8 Add MangaDex source 2020-04-26 20:22:49 +03:00
ef7108f6c9 Fix typo 2020-04-26 16:48:57 +03:00
941d992793 Merge branch 'devel' of https://github.com/nv95/Kotatsu into devel 2020-04-26 16:38:32 +03:00
de9a07a680 Select storage where save manga 2020-04-26 12:22:36 +03:00
0dc74f9188 Update readme 2020-04-25 18:06:11 +03:00
f95cf9b231 Reset new chapters on reading 2020-04-25 17:22:20 +03:00
838 changed files with 28132 additions and 11449 deletions

2
.gitignore vendored
View File

@@ -3,7 +3,9 @@
/local.properties
/.idea/caches
/.idea/libraries
/.idea/dictionaries
/.idea/modules.xml
/.idea/misc.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml

View File

@@ -23,6 +23,7 @@
</option>
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="CMake">

4
.idea/compiler.xml generated
View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel>
<module name="Kotatsu.app" target="1.8" />
</bytecodeTargetLevel>
<bytecodeTargetLevel target="11" />
</component>
</project>

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?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>

View File

@@ -1,13 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="admin">
<words>
<w>chucker</w>
<w>desu</w>
<w>koin</w>
<w>kotatsu</w>
<w>manga</w>
<w>upsert</w>
<w>webtoon</w>
</words>
</dictionary>
</component>

6
.idea/gradle.xml generated
View File

@@ -4,18 +4,16 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

View File

@@ -1,6 +1,9 @@
<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="KeySetIterationMayUseEntrySet" enabled="true" level="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>
</component>

View File

@@ -31,5 +31,10 @@
<option name="name" value="maven2" />
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

6
.idea/kotlinc.xml generated Normal file
View File

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

9
.idea/misc.xml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

9
.idea/vcs.xml generated
View File

@@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list>
<option value="master" />
<option value="devel" />
<option value="legacy" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>

View File

@@ -1,15 +1,11 @@
language: android
dist: trusty
jdk:
- oraclejdk8
android:
components:
- android-30
- build-tools-30.0.3
- platform-tools-30.0.5
- tools
- platform-tools-29.0.6
- build-tools-29.0.3
- android-29
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+
- google-gdk-license-.+
before_install:
- yes | sdkmanager "platforms;android-30"
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug

View File

@@ -2,29 +2,38 @@
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) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669)
![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)
### Download
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
Download APK from Github Releases:
- [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)
### Main Features
* Online manga catalogues
* Search manga by name and genre
* Reading history
* Favourites with custom categories
* Saving manga and reading it offline
* Tablet-optimized modern UI
* Reading third-party comics from CBZ
* Favourites organized by user-defined categories
* Downloading manga and reading it offline. Third-party CBZ archives also supported
* Tablet-optimized material design UI
* Standard and Webtoon-optimized reader
* Checking for new chapters
* Notifications about new chapters with updates feed
### Screenshots
| ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/75573590-d467f180-5a65-11ea-8338-a34af4679ed6.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/75573612-dcc02c80-5a65-11ea-9afb-293dadfb3cfd.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/75573621-e0ec4a00-5a65-11ea-92b9-72ab90281a2b.png) |
| ![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://user-images.githubusercontent.com/8948226/75573629-e34ea400-5a65-11ea-86a1-4496032ac0f0.png) | ![Screenshot_20200226-210151](https://user-images.githubusercontent.com/8948226/75573632-e5186780-5a65-11ea-81b0-7c296157709c.png) | ![Screenshot_20200226-210223](https://user-images.githubusercontent.com/8948226/75573639-e6e22b00-5a65-11ea-84a6-6257f532fd2c.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) |
| ![](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) |
|---|---|
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
@@ -37,4 +46,4 @@ published by the Free Software Foundation, either version 3 of the License, or
### Disclaimer
The developers of this application does not have any affiliation with the content providers available.
The developers of this application does not have any affiliation with the content providers available.

View File

@@ -1,100 +1,119 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
}
android {
compileSdkVersion 29
buildToolsVersion '29.0.3'
compileSdkVersion 31
buildToolsVersion '30.0.3'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 29
versionCode gitCommits
versionName '0.3'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
targetSdkVersion 31
versionCode 376
versionName '2.1'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg('room.schemaLocation', "$projectDir/schemas".toString())
arg 'room.schemaLocation', "$projectDir/schemas".toString()
}
}
}
archivesBaseName = "kotatsu_${gitCommits}"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
}
release {
multiDexEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
lintOptions {
disable 'MissingTranslation'
abortOnError false
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = true
unitTests.returnDefaultValues = false
}
}
androidExtensions {
experimental = true
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlin.contracts.ExperimentalContracts',
]
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation 'androidx.core:core-ktx:1.3.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.2.4'
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-service:2.4.0'
implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
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.1.1'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.4.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0'
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
implementation 'androidx.room:room-runtime:2.4.0'
implementation 'androidx.room:room-ktx:2.4.0'
kapt 'androidx.room:room-compiler:2.4.0'
implementation 'com.github.moxy-community:moxy:2.1.2'
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
implementation 'com.github.moxy-community:moxy-material:2.1.2'
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okio:okio:2.10.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okio:okio:2.5.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.1'
implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.9.5'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'
implementation 'io.insert-koin:koin-android:3.1.4'
implementation 'io.coil-kt:coil-base:1.4.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.3'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.google.truth:truth:1.1.3'
testImplementation 'org.json:json:20211205'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.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.0'
androidTestImplementation 'com.google.truth:truth:1.1.3'
}

View File

@@ -5,8 +5,7 @@
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
public <init>(...);
}
-dontwarn okhttp3.internal.platform.ConscryptPlatform

View File

@@ -0,0 +1,55 @@
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.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 {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
MangaDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
@Throws(IOException::class)
fun migrateAll() {
helper.createDatabase(TEST_DB, 1).apply {
// TODO execSQL("")
close()
}
for (migration in migrations) {
helper.runMigrationsAndValidate(
TEST_DB,
migration.endVersion,
true,
migration
)
}
}
private companion object {
const val TEST_DB = "test-db"
val migrations = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
)
}
}

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#E6000A">
<group android:scaleX="0.40188664"
android:scaleY="0.40188664"
android:translateX="32.90095"
android:translateY="18.7272">
<group android:translateY="139.39206">
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
android:fillColor="#E6000A"/>
</group>
</group>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name" translatable="false">Kotatsu Dev</string>
</resources>

View File

@@ -21,9 +21,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
<activity android:name=".ui.main.MainActivity">
<activity
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -32,50 +34,85 @@
android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" />
</activity>
<activity android:name=".ui.details.MangaDetailsActivity">
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
</activity>
<activity android:name=".ui.reader.ReaderActivity" />
<activity
android:name=".ui.search.SearchActivity"
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.READ_MANGA" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" />
<activity
android:name=".ui.settings.SettingsActivity"
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" />
<activity
android:name=".ui.reader.SimpleSettingsActivity"
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:exported="true"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ui.browser.BrowserActivity" />
<activity
android:name=".ui.utils.CrashActivity"
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:theme="@android:style/Theme.DeviceDefault"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:label="@string/favourites_categories" />
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
android:label="@string/favourites_categories"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:exported="true"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
android:label="@string/search" />
<activity
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
android:noHistory="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".settings.protect.ProtectSetupActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
android:label="@string/downloads" />
<activity android:name=".image.ui.ImageActivity"/>
<service
android:name=".ui.download.DownloadService"
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".ui.widget.recent.RecentWidgetService"
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name=".ui.search.MangaSuggestionsProvider"
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" />
<provider
@@ -87,24 +124,37 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<receiver android:name=".ui.widget.shelf.ShelfWidgetProvider"
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_shelf" />
</receiver>
<receiver android:name=".ui.widget.recent.RecentWidgetProvider"
<receiver
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:exported="true"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" />
</receiver>
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application>
</manifest>

View File

@@ -1,124 +1,98 @@
package org.koitharu.kotatsu
import android.app.Application
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room
import coil.Coil
import coil.ImageLoader
import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import okhttp3.OkHttpClient
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.parser.parserModule
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit
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.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() {
private val cookieJar by lazy {
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
}
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
ChuckerCollector(applicationContext)
}
override fun onCreate() {
super.onCreate()
initKoin()
initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
if (BuildConfig.DEBUG) {
initErrorHandler()
enableStrictMode()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
initKoin()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
val widgetUpdater = WidgetUpdater(applicationContext)
FavouritesRepository.subscribe(widgetUpdater)
HistoryRepository.subscribe(widgetUpdater)
widgetUpdater.subscribeToFavourites(get())
widgetUpdater.subscribeToHistory(get())
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(applicationContext)
androidContext(this@KotatsuApp)
modules(
module {
factory {
okHttp()
.cache(CacheUtils.createHttpCache(applicationContext))
.build()
}
single {
mangaDb().build()
}
single {
MangaLoaderContext()
}
factory {
AppSettings(applicationContext)
}
single {
PagesCache(applicationContext)
}
}
networkModule,
databaseModule,
githubModule,
uiModule,
parserModule,
mainModule,
searchModule,
localModule,
favouritesModule,
historyModule,
remoteListModule,
detailsModule,
trackerModule,
settingsModule,
readerModule,
appWidgetModule,
)
}
}
private fun initCoil() {
Coil.setDefaultImageLoader(ImageLoader(applicationContext) {
okHttpClient {
okHttp()
.cache(CoilUtils.createDefaultCache(applicationContext))
.build()
}
componentRegistry {
add(CbzFetcher())
}
})
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build()
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.build()
}
private fun initErrorHandler() {
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
chuckerCollector.onError("CRASH", e)
exceptionHandler?.uncaughtException(t, e)
}
}
private fun okHttp() = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
addInterceptor(UserAgentInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
}
}
private fun mangaDb() = Room.databaseBuilder(
applicationContext,
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(Migration1To2, Migration2To3, Migration3To4)
}

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.domain
package org.koitharu.kotatsu.base.domain
import androidx.room.withTransaction
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
@@ -10,9 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.ReaderMode
class MangaDataRepository : KoinComponent {
private val db: MangaDatabase by inject()
class MangaDataRepository(private val db: MangaDatabase) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
@@ -36,6 +32,12 @@ class MangaDataRepository : KoinComponent {
return db.mangaDao.find(mangaId)?.toManga()
}
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
else -> null // TODO resolve uri
}
suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction {

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.base.domain
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga
class MangaIntent(
val manga: Manga?,
val mangaId: Long,
val uri: Uri?
) {
companion object {
fun from(intent: Intent?) = MangaIntent(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
fun from(args: Bundle?) = MangaIntent(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
}
}

View File

@@ -0,0 +1,84 @@
package org.koitharu.kotatsu.base.domain
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.exceptions.GraphQLException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.parseJson
open class MangaLoaderContext(
private val okHttp: OkHttpClient,
val cookieJar: CookieJar,
) : KoinComponent {
suspend fun httpGet(url: String, headers: Headers? = null): Response {
val request = Request.Builder()
.get()
.url(url)
if (headers != null) {
request.headers(headers)
}
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
form: Map<String, String>,
): Response {
val body = FormBody.Builder()
form.forEach { (k, v) ->
body.addEncoded(k, v)
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
payload: String,
): Response {
val body = FormBody.Builder()
payload.split('&').forEach {
val pos = it.indexOf('=')
if (pos != -1) {
val k = it.substring(0, pos)
val v = it.substring(pos + 1)
body.addEncoded(k, v)
}
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
suspend fun graphQLQuery(endpoint: String, query: String): JSONObject {
val body = JSONObject()
body.put("operationName", null)
body.put("variables", JSONObject())
body.put("query", "{${query}}")
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = body.toString().toRequestBody(mediaType)
val request = Request.Builder()
.post(requestBody)
.url(endpoint)
val json = okHttp.newCall(request.build()).await().parseJson()
json.optJSONArray("errors")?.let {
if (it.length() != 0) {
throw GraphQLException(it)
}
}
return json
}
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.base.domain
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory {
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if (includeHidden) {
sorted
} else {
val hidden = settings.hiddenSources
sorted.filterNot { x ->
x.name in hidden
}
}
}
}

View File

@@ -0,0 +1,70 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.annotation.WorkerThread
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.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.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
*/
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CacheUtils.CONTROL_DISABLED)
.build()
client.newCall(request).await().use {
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)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
val binding = onInflateView(inflater, null)
viewBinding = binding
return AlertDialog.Builder(requireContext(), theme)
.setView(binding.root)
.also(::onBuildDialog)
.create()
}
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = viewBinding?.root
@CallSuper
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}

View File

@@ -0,0 +1,132 @@
package org.koitharu.kotatsu.base.ui
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
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.graphics.Insets
import androidx.core.view.*
import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
protected lateinit var binding: B
private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(this, supportFragmentManager)
}
private var lastInsets: Insets = Insets.NONE
override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) {
setTheme(R.style.AppTheme_AMOLED)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setupToolbar()
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(view: View?) {
super.setContentView(view)
setupToolbar()
}
protected fun setContentView(binding: B) {
this.binding = binding
super.setContentView(binding.root)
val toolbar = (binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)
toolbar?.let(this::setSupportActionBar)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
val toolbarParams = (binding.root.findViewById<View>(R.id.toolbar_card) ?: toolbar)
?.layoutParams as? AppBarLayout.LayoutParams
if (toolbarParams != null) {
if (get<AppSettings>().isToolbarHideWhenScrolling) {
toolbarParams.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP
} else {
toolbarParams.scrollFlags = SCROLL_FLAG_NO_SCROLL
}
}
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val baseInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val newInsets = Insets.max(baseInsets, imeInsets)
if (newInsets != lastInsets) {
onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return insets
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
onBackPressed()
true
} else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
ActivityCompat.recreate(this)
return true
}
return super.onKeyDown(keyCode, event)
}
protected abstract fun onWindowInsetsChanged(insets: Insets)
private fun setupToolbar() {
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
}
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
}
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
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
}
}
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
isTaskRoot &&
supportFragmentManager.backStackEntryCount == 0
) {
finishAfterTransition()
} else {
super.onBackPressed()
}
}
}

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialog
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
abstract class BaseBottomSheet<B : ViewBinding> :
BottomSheetDialogFragment() {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = onInflateView(inflater, container)
viewBinding = binding
return binding.root
}
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, theme)
} else super.onCreateDialog(savedInstanceState)
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}

View File

@@ -0,0 +1,73 @@
package org.koitharu.kotatsu.base.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
}
private var lastInsets: Insets = Insets.NONE
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = onInflateView(inflater, container)
viewBinding = binding
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lastInsets = Insets.NONE
ViewCompat.setOnApplyWindowInsetsListener(view, this)
}
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) {
super.onAttach(context)
getTitle()?.let {
activity?.title = it
}
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
if (newInsets != lastInsets) {
onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return insets
}
protected fun bindingOrNull() = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
protected abstract fun onWindowInsetsChanged(insets: Insets)
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.base.ui
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(window) {
statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
}
showSystemUI()
}
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
@Suppress("DEPRECATION")
private companion object {
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
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
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.base.ui
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listView.clipToPadding = false
ViewCompat.setOnApplyWindowInsetsListener(view, this)
}
override fun onResume() {
super.onResume()
activity?.setTitle(titleId)
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
listView.updatePadding(
left = systemBars.left,
right = systemBars.right,
bottom = systemBars.bottom
)
return insets
}
}

View File

@@ -0,0 +1,5 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LifecycleService
abstract class BaseService : LifecycleService()

View File

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

View File

@@ -1,14 +1,12 @@
package org.koitharu.kotatsu.ui.common.dialog
package org.koitharu.kotatsu.base.ui.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.checkbox.MaterialCheckBox
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
@@ -17,13 +15,10 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
class Builder(context: Context) {
@SuppressLint("InflateParams")
private val view = LayoutInflater.from(context)
.inflate(R.layout.dialog_checkbox, null, false)
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
private val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context)
.setView(view)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
@@ -46,12 +41,12 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
}
fun setCheckBoxText(@StringRes textId: Int): Builder {
checkBox.setText(textId)
binding.checkbox.setText(textId)
return this
}
fun setCheckBoxChecked(isChecked: Boolean): Builder {
checkBox.isChecked = isChecked
binding.checkbox.isChecked = isChecked
return this
}
@@ -65,7 +60,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
listener: (DialogInterface, Boolean) -> Unit
): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, checkBox.isChecked)
listener(dialog, binding.checkbox.isChecked)
}
return this
}

View File

@@ -0,0 +1,94 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context)
private val delegate = AlertDialog.Builder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val checked = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setSingleChoiceItems(adapter, checked) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
}
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setNegativeButton(@StringRes textId: Int): Builder {
delegate.setNegativeButton(textId, null)
return this
}
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(context: Context) : BaseAdapter() {
val volumes = getAvailableVolumes(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: parent.inflate(R.layout.item_storage)
val item = volumes[position]
val binding = ItemStorageBinding.bind(view)
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
}
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = volumes[position].first.absolutePath.longHashCode()
override fun getCount() = volumes.size
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
private companion object {
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
return LocalMangaRepository.getAvailableStorageDirs(context).map {
it to it.getStorageName(context)
}
}
}
}

View File

@@ -1,27 +1,25 @@
package org.koitharu.kotatsu.ui.common.dialog
package org.koitharu.kotatsu.base.ui.dialog
import android.annotation.SuppressLint
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 kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
class TextInputDialog private constructor(
private val delegate: AlertDialog,
) : DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context) {
@SuppressLint("InflateParams")
private val view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null, false)
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context)
.setView(view)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
@@ -34,44 +32,55 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
}
fun setHint(@StringRes hintResId: Int): Builder {
view.inputLayout.hint = view.context.getString(hintResId)
binding.inputEdit.hint = binding.root.context.getString(hintResId)
return this
}
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
with(view.inputLayout) {
with(binding.inputLayout) {
counterMaxLength = maxLength
isCounterEnabled = maxLength > 0
}
if (strict && maxLength > 0) {
view.inputEdit.filters += InputFilter.LengthFilter(maxLength)
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
}
return this
}
fun setInputType(inputType: Int): Builder {
view.inputEdit.inputType = inputType
binding.inputEdit.inputType = inputType
return this
}
fun setText(text: String): Builder {
view.inputEdit.setText(text)
view.inputEdit.setSelection(text.length)
binding.inputEdit.setText(text)
binding.inputEdit.setSelection(text.length)
return this
}
fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder {
fun setPositiveButton(
@StringRes textId: Int,
listener: (DialogInterface, String) -> Unit
): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, view.inputEdit.text?.toString().orEmpty())
listener(dialog, binding.inputEdit.text?.toString().orEmpty())
}
return this
}
fun setNegativeButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener? = null): Builder {
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

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.common.list
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -12,13 +12,15 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
super.onScrolled(recyclerView, dx, dy)
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
return
}
if (firstVisibleItemPosition <= offsetTop) {
onScrolledToStart(recyclerView)
return
}
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom && firstVisibleItemPosition >= 0) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
onScrolledToEnd(recyclerView)
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
interface OnListItemClickListener<I> {
fun onItemClick(item: I, view: View)
fun onItemLongClick(item: I, view: View) = false
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
class PaginationScrollListener(offset: Int, private val callback: Callback) :
BoundsScrollListener(0, offset) {
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
override fun onScrolledToEnd(recyclerView: RecyclerView) {
callback.onScrolledToEnd()
}
interface Callback {
fun onScrolledToEnd()
}
}

View File

@@ -0,0 +1,65 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import kotlin.math.roundToInt
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val divider = context.getThemeDrawable(android.R.attr.listDivider)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0)
}
// TODO implement for horizontal lists on demand
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
if (parent.layoutManager == null || divider == null) {
return
}
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
)
} else {
left = 0
right = parent.width
}
var previous: RecyclerView.ViewHolder? = null
for (child in parent.children) {
val holder = parent.getChildViewHolder(child)
if (previous != null && shouldDrawDivider(previous, holder)) {
parent.getDecoratedBoundsWithMargins(child, bounds)
val top: Int = bounds.top + child.translationY.roundToInt()
val bottom: Int = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(canvas)
}
previous = holder
}
canvas.restore()
}
protected abstract fun shouldDrawDivider(
above: RecyclerView.ViewHolder,
below: RecyclerView.ViewHolder,
): Boolean
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.common.list.decor
package org.koitharu.kotatsu.base.ui.list.decor
import android.content.Context
import android.graphics.Canvas

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.view.View
import androidx.annotation.Px
import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.set(spacing, spacing, spacing, spacing)
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isGone
import com.google.android.material.R
import com.google.android.material.appbar.MaterialToolbar
import java.lang.reflect.Field
class AnimatedToolbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.toolbarStyle,
) : MaterialToolbar(context, attrs, defStyleAttr) {
private var navButtonView: View? = null
get() {
if (field == null) {
runCatching {
field = navButtonViewField?.get(this) as? View
}
}
return field
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
navButtonView?.isGone = (icon == null)
}
private companion object {
val navButtonViewField: Field? = runCatching {
Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}.getOrNull()
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.common.widgets
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
@@ -54,14 +54,13 @@ class CheckableImageView @JvmOverloads constructor(
return state
}
interface OnCheckedChangeListener {
fun interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
}
private companion object {
@JvmStatic
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@@ -0,0 +1,135 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.view.children
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
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
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 {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
private var chipOnCloseListener = OnClickListener {
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
}
var onChipClickListener: OnChipClickListener? = null
set(value) {
field = value
val isChipClickable = value != null
children.forEach { it.isClickable = isChipClickable }
}
var onChipCloseClickListener: OnChipCloseClickListener? = null
set(value) {
field = value
val isCloseIconVisible = value != null
children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible }
}
override fun requestLayout() {
if (isLayoutSuppressedCompat) {
isLayoutCalledOnSuppressed = true
} else {
super.requestLayout()
}
}
fun setChips(items: Collection<ChipModel>) {
suppressLayoutCompat(true)
try {
for ((i, model) in items.withIndex()) {
val chip = getChildAt(i) as Chip? ?: addChip()
bindChip(chip, model)
}
if (childCount > items.size) {
removeViews(items.size, childCount - items.size)
}
} finally {
suppressLayoutCompat(false)
}
}
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
if (model.icon == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.setChipIconResource(model.icon)
}
chip.isClickable = onChipClickListener != null
chip.tag = model.data
}
private fun addChip(): Chip {
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
chip.setChipDrawable(drawable)
chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary))
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
addView(chip)
return chip
}
private fun suppressLayoutCompat(suppress: Boolean) {
isLayoutSuppressedCompat = suppress
if (!suppress) {
if (isLayoutCalledOnSuppressed) {
requestLayout()
isLayoutCalledOnSuppressed = false
}
}
}
class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ChipModel
if (icon != other.icon) return false
if (title != other.title) return false
if (data != other.data) return false
return true
}
override fun hashCode(): Int {
var result = icon
result = 31 * result + title.hashCode()
result = 31 * result + data.hashCode()
return result
}
}
fun interface OnChipClickListener {
fun onChipClick(chip: Chip, data: Any?)
}
fun interface OnChipCloseClickListener {
fun onChipCloseClick(chip: Chip, data: Any?)
}
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.withStyledAttributes
import org.koitharu.kotatsu.R
import kotlin.math.roundToInt
class CoverImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr) {
private var orientation: Int = HORIZONTAL
init {
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth: Int
val desiredHeight: Int
if (orientation == VERTICAL) {
desiredHeight = measuredHeight
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
} else {
desiredWidth = measuredWidth
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
}
setMeasuredDimension(desiredWidth, desiredHeight)
}
companion object {
const val VERTICAL = LinearLayout.VERTICAL
const val HORIZONTAL = LinearLayout.HORIZONTAL
private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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
/**
* 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()
}
}
companion object {
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
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.common.widgets
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.browser
package org.koitharu.kotatsu.browser
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
@@ -8,47 +8,53 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_browser.*
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity(), BrowserCallback {
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_browser)
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_cross)
}
with(webView.settings) {
with(binding.webView.settings) {
javaScriptEnabled = true
}
webView.webViewClient = BrowserClient(this)
binding.webView.webViewClient = BrowserClient(this)
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finish()
finishAfterTransition()
} else {
webView.loadUrl(url)
onTitleChanged(
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
url
)
binding.webView.loadUrl(url)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
webView.stopLoading()
finish()
binding.webView.stopLoading()
finishAfterTransition()
true
}
R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(webView.url)
intent.data = Uri.parse(binding.webView.url)
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
@@ -59,25 +65,25 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
if (binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
webView.onPause()
binding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
webView.onResume()
binding.webView.onResume()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
binding.progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
@@ -85,10 +91,27 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
supportActionBar?.subtitle = subtitle
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(
top = insets.top,
left = insets.left,
right = insets.right,
)
binding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
companion object {
@JvmStatic
fun newIntent(context: Context, url: String) = Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
private const val EXTRA_TITLE = "title"
fun newIntent(context: Context, url: String, title: String?): Intent {
return Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
.putExtra(EXTRA_TITLE, title)
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.browser
package org.koitharu.kotatsu.browser
interface BrowserCallback {

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebView
import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.core.network.WebViewClientCompat
class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat(), KoinComponent {
private val okHttp by inject<OkHttpClient>(mode = LazyThreadSafetyMode.SYNCHRONIZED)
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
callback.onLoadingStateChanged(isLoading = false)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
callback.onLoadingStateChanged(isLoading = true)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.browser.cloudflare
interface CloudFlareCallback {
fun onPageLoaded()
fun onCheckPassed()
}

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.WebView
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.WebViewClientCompat
class CloudFlareClient(
private val cookieJar: AndroidCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String
) : WebViewClientCompat() {
private val oldClearance = getCookieValue(CF_CLEARANCE)
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
checkClearance()
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
callback.onPageLoaded()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
callback.onPageLoaded()
}
private fun checkClearance() {
val clearance = getCookieValue(CF_CLEARANCE)
if (clearance != null && clearance != oldClearance) {
callback.onCheckPassed()
}
}
private fun getCookieValue(name: String): String? {
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
.find { it.name == name }?.value
}
private companion object {
const val CF_CLEARANCE = "cf_clearance"
}
}

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private val url by stringArgument(ARG_URL)
private val pendingResult = Bundle(1)
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent
}
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
if (url.isNullOrEmpty()) {
dismissAllowingStateLoss()
} else {
binding.webView.loadUrl(url.orEmpty())
}
}
override fun onDestroyView() {
binding.webView.stopLoading()
super.onDestroyView()
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setNegativeButton(android.R.string.cancel, null)
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
}
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(TAG, pendingResult)
super.onDismiss(dialog)
}
override fun onPageLoaded() {
bindingOrNull()?.progressBar?.isInvisible = true
}
override fun onCheckPassed() {
pendingResult.putBoolean(EXTRA_RESULT, true)
dismiss()
}
companion object {
const val TAG = "CloudFlareDialog"
const val EXTRA_RESULT = "result"
private const val ARG_URL = "url"
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
putString(ARG_URL, url)
}
}
}

View File

@@ -0,0 +1,51 @@
package org.koitharu.kotatsu.core.backup
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.MutableZipFile
import org.koitharu.kotatsu.utils.ext.format
import java.io.File
import java.util.*
class BackupArchive(file: File) : MutableZipFile(file) {
init {
if (!dir.exists()) {
dir.mkdirs()
}
}
suspend fun put(entry: BackupEntry) {
put(entry.name, entry.data.toString(2))
}
suspend fun getEntry(name: String): BackupEntry {
val json = withContext(Dispatchers.Default) {
JSONArray(getContent(name))
}
return BackupEntry(name, json)
}
companion object {
private const val DIR_BACKUPS = "backups"
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}
dir.mkdirs()
val filename = buildString {
append(context.getString(R.string.app_name).toLowerCase(Locale.ROOT))
append('_')
append(Date().format("ddMMyyyy"))
append(".bak")
}
BackupArchive(File(dir, filename))
}
}
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
class BackupEntry(
val name: String,
val data: JSONArray
) {
companion object Names {
const val INDEX = "index"
const val HISTORY = "history"
const val CATEGORIES = "categories"
const val FAVOURITES = "favourites"
}
}

View File

@@ -0,0 +1,137 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
class BackupRepository(private val db: MangaDatabase) {
suspend fun dumpHistory(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
while (true) {
val history = db.historyDao.findAll(offset, PAGE_SIZE)
if (history.isEmpty()) {
break
}
offset += history.size
for (item in history) {
val manga = item.manga.toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
manga.put("tags", tags)
val json = item.history.toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val categories = db.favouriteCategoriesDao.findAll()
for (item in categories) {
entry.data.put(item.toJson())
}
return entry
}
suspend fun dumpFavourites(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
while (true) {
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
if (favourites.isEmpty()) {
break
}
offset += favourites.size
for (item in favourites) {
val manga = item.manga.toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
manga.put("tags", tags)
val json = item.favourite.toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE)
json.put("created_at", System.currentTimeMillis())
entry.data.put(json)
return entry
}
private fun MangaEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("alt_title", altTitle)
jo.put("url", url)
jo.put("public_url", publicUrl)
jo.put("rating", rating)
jo.put("nsfw", isNsfw)
jo.put("cover_url", coverUrl)
jo.put("large_cover_url", largeCoverUrl)
jo.put("state", state)
jo.put("author", author)
jo.put("source", source)
return jo
}
private fun TagEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("key", key)
jo.put("source", source)
return jo
}
private fun HistoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("created_at", createdAt)
jo.put("updated_at", updatedAt)
jo.put("chapter_id", chapterId)
jo.put("page", page)
jo.put("scroll", scroll)
return jo
}
private fun FavouriteCategoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
jo.put("sort_key", sortKey)
jo.put("title", title)
jo.put("order", order)
return jo
}
private fun FavouriteEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
return jo
}
private companion object {
const val PAGE_SIZE = 10
}
}

View File

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

View File

@@ -0,0 +1,114 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.utils.ext.getBooleanOrDefault
import org.koitharu.kotatsu.utils.ext.getStringOrNull
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.map
class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
parseTag(it)
}
val history = parseHistory(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.historyDao.upsert(history)
}
}
}
return result
}
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val category = parseCategory(item)
result += runCatching {
db.favouriteCategoriesDao.upsert(category)
}
}
return result
}
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
parseTag(it)
}
val favourite = parseFavourite(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.favouritesDao.upsert(favourite)
}
}
}
return result
}
private fun parseManga(json: JSONObject) = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
source = json.getString("source")
)
private fun parseTag(json: JSONObject) = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source")
)
private fun parseHistory(json: JSONObject) = HistoryEntity(
mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat()
)
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title"),
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
createdAt = json.getLong("created_at")
)
}

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.migrations.*
val databaseModule
get() = module {
single {
Room.databaseBuilder(
androidContext(),
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
).addCallback(
DatabasePrePopulateCallback(androidContext().resources)
).build()
}
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.db
import android.content.res.Resources
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.SortOrder
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)",
arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name)
)
}
}

View File

@@ -1,23 +0,0 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
@Dao
abstract class FavouriteCategoriesDao {
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy")
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
abstract suspend fun delete(id: Long)
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String)
}

View File

@@ -1,27 +0,0 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao
abstract class FavouritesDao {
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY :orderBy LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga?
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun add(favourite: FavouriteEntity)
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
abstract suspend fun delete(categoryId: Long, mangaId: Long)
}

View File

@@ -1,47 +0,0 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao
abstract class HistoryDao {
/**
* @hide
*/
@Transaction
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity?
@Query("DELETE FROM history")
abstract suspend fun clear()
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
@Query("DELETE FROM history WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
@Transaction
open suspend fun upsert(entity: HistoryEntity): Boolean {
return if (update(entity) == 0) {
insert(entity)
true
} else false
}
}

View File

@@ -2,13 +2,21 @@ package org.koitharu.kotatsu.core.db
import androidx.room.Database
import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
], version = 4
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
], version = 9
)
abstract class MangaDatabase : RoomDatabase() {
@@ -25,4 +33,6 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@@ -13,6 +13,14 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags?
@Transaction
@Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
@Transaction
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(manga: MangaEntity): Long

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity

View File

@@ -1,22 +1,22 @@
package org.koitharu.kotatsu.core.db
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
interface TagsDao {
abstract class TagsDao {
@Query("SELECT * FROM tags")
suspend fun getAllTags(): List<TagEntity>
abstract suspend fun getAllTags(): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(tag: TagEntity): Long
abstract suspend fun insert(tag: TagEntity): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
suspend fun update(tag: TagEntity): Int
abstract suspend fun update(tag: TagEntity): Int
@Transaction
suspend fun upsert(tags: Iterable<TagEntity>) {
open suspend fun upsert(tags: Iterable<TagEntity>) {
tags.forEach { tag ->
if (update(tag) <= 0) {
insert(tag)

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga
@Dao
interface TrackLogsDao {
@Transaction
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
suspend fun findAll(offset: Int, limit: Int): List<TrackLogWithManga>
@Query("DELETE FROM track_logs")
suspend fun clear()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackEntity
@@ -25,11 +25,13 @@ abstract class TracksDao {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
@Transaction
open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) {
insert(entity)
}
}
}

View File

@@ -9,17 +9,19 @@ import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.core.model.MangaTag
@Entity(tableName = "manga")
data class MangaEntity(
class MangaEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
@ColumnInfo(name = "alt_title") val altTitle: String?,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
@ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float, //normalized value [0..1] or -1
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
@ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
@ColumnInfo(name = "state") val state: String? = null,
@ColumnInfo(name = "author") val author: String? = null,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "source") val source: String
) {
@@ -29,7 +31,9 @@ data class MangaEntity(
altTitle = this.altTitle,
state = this.state?.let { MangaState.valueOf(it) },
rating = this.rating,
isNsfw = this.isNsfw,
url = this.url,
publicUrl = this.publicUrl,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
@@ -42,11 +46,13 @@ data class MangaEntity(
fun from(manga: Manga) = MangaEntity(
id = manga.id,
url = manga.url,
publicUrl = manga.publicUrl,
source = manga.source.name,
largeCoverUrl = manga.largeCoverUrl,
coverUrl = manga.coverUrl,
altTitle = manga.altTitle,
rating = manga.rating,
isNsfw = manga.isNsfw,
state = manga.state?.name,
title = manga.title,
author = manga.author

View File

@@ -14,7 +14,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE
)]
)
data class MangaPrefsEntity(
class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "mode") val mode: Int

View File

@@ -20,7 +20,7 @@ import androidx.room.ForeignKey
)
]
)
data class MangaTagsEntity(
class MangaTagsEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
)

View File

@@ -3,8 +3,9 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet
data class MangaWithTags(
class MangaWithTags(
@Embedded val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
@@ -14,7 +15,7 @@ data class MangaWithTags(
val tags: List<TagEntity>
) {
fun toManga() = manga.toManga(tags.map {
fun toManga() = manga.toManga(tags.mapToSet {
it.toMangaTag()
}.toSet())
})
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "suggestions",
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
class SuggestionEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "relevance") val relevance: Float,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
)

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode
@Entity(tableName = "tags")
data class TagEntity(
class TagEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String,

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
)
]
)
data class TrackEntity (
class TrackEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int,

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "track_logs", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
class TrackLogEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "chapters") val chapters: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id"
)
val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
) {
fun toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.mapToSet { x -> x.toMangaTag() }),
createdAt = Date(trackLog.createdAt)
)
}

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration1To2 : Migration(1, 2) {
class Migration1To2 : Migration(1, 2) {
/**
* Adding foreign keys
*/

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration2To3 : Migration(2, 3) {
class Migration2To3 : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration3To4 : Migration(3, 4) {
class Migration3To4 : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration4To5 : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration5To6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration6To7 : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
}
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration7To8 : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
database.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.core.model.SortOrder
class Migration8To9 : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
}
}

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