Compare commits

...

241 Commits
5.1.3 ... 5.3.8

Author SHA1 Message Date
tom5079
c9519ec681 Fix image not retrying 2022-07-19 20:29:39 -07:00
tom5079
b146ed684d Fix app crashing when recovering metadata is corrupt 2022-05-31 08:06:48 +09:00
tom5079
d2787c36d7 Update README.md 2022-04-24 20:39:17 +09:00
tom5079
3ff663114a 5.3.7 2022-04-24 20:39:01 +09:00
tom5079
573e62f310 5.3.7 2022-04-24 20:36:56 +09:00
tom5079
f9af670b82 Update README.md 2022-04-24 20:21:41 +09:00
tom5079
bf461475c6 Merge remote-tracking branch 'origin/master' 2022-04-24 20:20:55 +09:00
tom5079
bdea6e0cc1 Use System.currentTimeMillis() instead of Instant 2022-04-24 20:20:45 +09:00
tom5079
57f0ec4e5d Update README.md 2022-04-24 18:44:10 +09:00
tom5079
d663092363 v5.3.5 2022-04-24 18:27:56 +09:00
tom5079
edf6188e36 Merge pull request #126 from tom5079/Pupil-116
Pupil 116 Favorite tag backup
2022-04-24 18:08:13 +09:00
tom5079
f3f3395e68 Merge pull request #128 from tom5079/Pupil-127
Pupil-127 Use gg.js directly
2022-04-24 18:08:04 +09:00
tom5079
ac9dc347e3 Pupil-127 Use gg.js directly 2022-04-22 16:39:13 +09:00
tom5079
8721d85946 Show ProgressDrawable when backup 2022-04-21 17:41:06 +09:00
tom5079
a0bd1a8738 Pupil-116 Add favorite tags backup 2022-04-21 17:26:58 +09:00
tom5079
35fdf3e3b0 Update PreferenceFragments to comply with updated library 2022-04-21 16:49:13 +09:00
tom5079
aced8293f1 Dependency Update 2022-04-21 16:45:13 +09:00
tom5079
3f516faad8 AGP update 2022-04-21 16:42:34 +09:00
tom5079
824f7b9602 Merge remote-tracking branch 'origin/master' 2022-04-16 06:30:31 +09:00
tom5079
95aeeaa16f updated .gitignore 2022-04-16 06:30:10 +09:00
tom5079
63f08f0230 Fixed Downloaded folder gets deleted when opened with no network 2022-03-25 16:48:38 +09:00
tom5079
3b241fe857 Delete watchdiff.yml 2022-02-02 06:38:50 +09:00
tom5079
75bc104f43 Update watchdiff.yml 2022-02-02 05:57:55 +09:00
tom5079
30afd56324 Update README.md 2022-02-01 19:11:55 +09:00
tom5079
5ee1bb11a0 Merge remote-tracking branch 'origin/master' 2022-02-01 19:11:25 +09:00
tom5079
c1de45abce use webp by default 2022-02-01 19:10:54 +09:00
tom5079
8805033c8d Update README.md 2022-02-01 17:47:46 +09:00
tom5079
0ed59bb8a9 Merge remote-tracking branch 'origin/master' 2022-02-01 17:46:44 +09:00
tom5079
8163f2fd28 Bug fix 2022-02-01 17:46:35 +09:00
tom5079
521a65c9d2 Update README.md 2022-02-01 17:37:50 +09:00
tom5079
eb98424668 Bug fix 2022-02-01 17:36:44 +09:00
tom5079
961c731743 Merge remote-tracking branch 'origin/master' 2022-02-01 11:45:54 +09:00
tom5079
5188769fb6 Fuck hitomi 2022-02-01 11:45:45 +09:00
tom5079
8f27d9e30f Update README.md 2022-02-01 11:45:35 +09:00
tom5079
b58566999e Update README.md 2022-02-01 11:40:23 +09:00
tom5079
117d6dcd2b Fuck hitomi 2022-02-01 11:39:26 +09:00
tom5079
2608796929 Remove dumb code 2022-01-31 13:28:31 +09:00
tom5079
792f5b5a7f Fixed downloading after revisiting cached manga 2022-01-31 13:27:23 +09:00
tom5079
a77b1db749 Merge remote-tracking branch 'origin/master' 2022-01-31 13:18:28 +09:00
tom5079
9d984d92af Fixed downloading after revisiting cached manga 2022-01-31 13:17:44 +09:00
tom5079
e303f25991 Update README.md 2022-01-31 11:10:02 +09:00
tom5079
85973d2305 Merge remote-tracking branch 'origin/master' 2022-01-31 11:05:05 +09:00
tom5079
13f8d7b747 Added download database recovery 2022-01-31 11:04:52 +09:00
tom5079
e198860edb Update README.md 2022-01-31 07:39:58 +09:00
tom5079
fc8355467b Fixed autoupdate for Android 5 and 6 2022-01-31 01:46:22 +09:00
tom5079
67abc15442 Merge remote-tracking branch 'origin/master' 2022-01-31 01:03:26 +09:00
tom5079
e94cddb86a Hitomi is stupid enough to block user agent for chrome... holy shit
Added self-test and reload
Reduced update ignoring time to 1d from 1w
2022-01-31 01:02:47 +09:00
tom5079
700f7a33a5 Update README.md 2022-01-25 05:00:04 +09:00
tom5079
41e952144d Merge remote-tracking branch 'origin/master' 2022-01-25 04:59:37 +09:00
tom5079
910ed65937 Improve startup speed 2022-01-25 04:59:25 +09:00
tom5079
e06701a2fb Update README.md 2022-01-25 04:30:24 +09:00
tom5079
62dce26c73 Merge remote-tracking branch 'origin/master' 2022-01-25 04:28:16 +09:00
tom5079
ac0cff62d4 Ask user to update WebView when es2020 is not supported 2022-01-25 04:28:04 +09:00
tom5079
655c060814 Drop Guava from dependency 2022-01-22 10:00:28 +09:00
tom5079
36d27895e7 Update README.md 2022-01-21 17:11:03 +09:00
tom5079
803481f74c Merge remote-tracking branch 'origin/master' 2022-01-21 17:08:57 +09:00
tom5079
b3ca1686e3 5.2.19
Improved error report
Lenient JSON decoding
2022-01-21 17:08:49 +09:00
tom5079
8f220eb0cb Update README.md 2022-01-20 19:42:41 +09:00
tom5079
51d5f42e8b Merge remote-tracking branch 'origin/master' 2022-01-20 19:41:22 +09:00
tom5079
8d8c5ace61 Fixed {} 2022-01-20 19:41:10 +09:00
tom5079
4bb6b8ccc9 Update README.md 2022-01-20 18:06:28 +09:00
tom5079
6bebd36e83 Merge remote-tracking branch 'origin/master' 2022-01-20 18:05:27 +09:00
tom5079
edc7053e50 Optimize Firebase 2022-01-20 18:05:18 +09:00
tom5079
55e6ef5f78 Update README.md 2022-01-20 16:06:26 +09:00
tom5079
9781d7a5dc Merge remote-tracking branch 'origin/master' 2022-01-20 16:05:31 +09:00
tom5079
b83cf87cd8 Updated proguard-rules.pro 2022-01-20 16:05:22 +09:00
tom5079
430864512d Update README.md 2022-01-20 15:58:17 +09:00
tom5079
16eeef1878 Merge remote-tracking branch 'origin/master' 2022-01-20 15:57:54 +09:00
tom5079
994d4b589b 5.2.15 Fixed thumbnail not loading 2022-01-20 15:57:43 +09:00
tom5079
43adba6f13 Update README.md 2022-01-18 18:26:14 +09:00
tom5079
e4fbd21731 Update README.md 2022-01-17 00:46:20 +09:00
tom5079
8be64745fc Fix thumbnail 2022-01-17 00:45:20 +09:00
tom5079
b66f376729 Merge remote-tracking branch 'origin/master' 2022-01-17 00:45:13 +09:00
tom5079
cc40416e1e Improved loading speed
Fixed images not loading
2022-01-17 00:35:15 +09:00
tom5079
5073352366 Update README.md 2022-01-16 11:29:36 +09:00
tom5079
9ae12a2c4c Merge remote-tracking branch 'origin/master' 2022-01-16 11:29:21 +09:00
tom5079
843b8412a9 5.2.13 Fixed thumbnails not loading 2022-01-16 11:29:06 +09:00
tom5079
4f67578371 Update README.md 2022-01-11 17:16:20 +09:00
tom5079
37f2227093 Merge remote-tracking branch 'origin/master' 2022-01-11 17:12:10 +09:00
tom5079
1833c0bde5 5.1.12 Improved suggestion loading speed / Fixed images not loading 2022-01-11 17:11:59 +09:00
tom5079
aa3aeca3f2 Update README.md 2022-01-11 12:25:43 +09:00
tom5079
152d4e248f Removed runBlocking from codebase 2022-01-11 12:21:43 +09:00
tom5079
7461c8d201 Merge remote-tracking branch 'origin/master' 2022-01-09 00:34:38 +09:00
tom5079
0902fdf981 Improved search speed 2022-01-09 00:34:29 +09:00
tom5079
0fd2cf4fd7 Removed logs 2022-01-08 18:33:48 +09:00
tom5079
679558106f Update README.md 2022-01-08 10:20:00 +09:00
tom5079
e498efc493 Fixed Download location dialog keep popping up 2022-01-08 10:13:20 +09:00
tom5079
74bbc71741 Fixed thumbnail not loading 2022-01-08 10:06:45 +09:00
tom5079
502b4890e3 5.2.8 Fix for loading not finishing 2022-01-07 18:47:07 +09:00
tom5079
dfb60461e4 Merge remote-tracking branch 'origin/master' 2022-01-05 20:20:08 +09:00
tom5079
bd6bc418e6 5.2.8-BETA01 potential fix for loading not finishing 2022-01-05 20:19:00 +09:00
tom5079
a284143ce1 Update README.md 2022-01-04 23:18:16 +09:00
tom5079
1f1c782772 5.2.7 Fix app crashing on Android 12 2022-01-04 23:15:34 +09:00
tom5079
5c0f5fe333 5.2.6 Dependency update & Fixes the bug (The problem was kotlinx.serialization!!!) 2022-01-04 22:50:16 +09:00
tom5079
748e023fde 5.2.5 Added logging to fix app crashing 2022-01-04 20:30:45 +09:00
tom5079
30104bacd2 Update README.md 2022-01-04 20:16:41 +09:00
tom5079
f33d1a1bfa 5.2.4 Added logging to fix app crashing 2022-01-04 20:16:04 +09:00
tom5079
3c08331441 5.2.3 Added logging to fix app crashing 2022-01-04 19:57:00 +09:00
tom5079
3eaa38247b 5.2.2 Fixed app crashing 2022-01-04 19:10:58 +09:00
tom5079
304ce643f9 Update README.md 2022-01-03 17:15:56 +09:00
tom5079
b4ad994f95 Create watchdiff.yml 2022-01-03 15:36:00 +09:00
tom5079
03c5cfa791 Fixed image not loading 2022-01-03 14:46:22 +09:00
tom5079
e8056072b8 Merge remote-tracking branch 'origin/master' 2022-01-03 14:31:50 +09:00
tom5079
d134639a5f Update README.md 2022-01-03 11:45:55 +09:00
tom5079
b4745d76b8 Update README.md 2022-01-03 09:18:38 +09:00
tom5079
c5fd674020 User-Agent 2022-01-03 00:00:37 +09:00
tom5079
9b821dd7cb Update README.md 2022-01-02 23:49:58 +09:00
tom5079
1b441f6aea Migrate to coroutine 2022-01-02 20:32:00 +09:00
tom5079
213902c854 Update README.md 2022-01-02 16:46:54 +09:00
tom5079
2054922586 Update README.md 2022-01-02 16:46:43 +09:00
tom5079
a17b7355f5 Merge remote-tracking branch 'origin/master' 2022-01-02 15:30:20 +09:00
tom5079
066a1e1f3a use WebView(!) as a js engine 2022-01-02 15:30:03 +09:00
tom5079
b10cbfbd63 Update README.md 2022-01-02 15:18:11 +09:00
tom5079
fcd72bb8bd Revert back to quickjs-android (quickjs stackoverflows) 2022-01-02 09:16:28 +09:00
tom5079
37cd99731c Fixed images not loading 2022-01-02 09:08:53 +09:00
tom5079
ed97773f24 Update README.md 2022-01-01 16:58:31 +09:00
tom5079
0424ba3e87 Merge remote-tracking branch 'origin/master' 2022-01-01 16:58:08 +09:00
tom5079
9539c4e7bf Fixed some images not loading 2022-01-01 16:57:55 +09:00
tom5079
248b378f01 Fixed some images not loading 2022-01-01 16:56:15 +09:00
tom5079
1c40575665 Fixed images not loading 2022-01-01 08:34:47 +09:00
tom5079
ac67c648be Update README.md 2022-01-01 02:07:12 +09:00
tom5079
42cc026acc Merge remote-tracking branch 'origin/master' 2021-12-31 15:37:37 +09:00
tom5079
23a74edfad Forgot to change version 2021-12-31 15:37:29 +09:00
tom5079
5da1804f17 Update README.md 2021-12-31 15:33:21 +09:00
tom5079
75f0c35017 Merge remote-tracking branch 'origin/master' 2021-12-31 15:29:05 +09:00
tom5079
0e6b02d260 Dependency update 2021-12-31 15:28:44 +09:00
tom5079
d5a0ce55f0 Update README.md 2021-12-31 14:47:28 +09:00
tom5079
09fc6fe8ef Merge remote-tracking branch 'origin/master' 2021-12-31 14:40:45 +09:00
tom5079
ff30be879a Dependency update 2021-12-31 14:40:31 +09:00
tom5079
309fe4d831 Update README.md 2021-12-29 20:28:52 +09:00
tom5079
dff0c817a7 Merge remote-tracking branch 'origin/master' 2021-12-29 20:28:38 +09:00
tom5079
04313981d4 5.1.22 Fixed gallery thumbnail not visible 2021-12-29 20:28:18 +09:00
tom5079
810cb4d13a Update README.md 2021-12-29 11:37:02 +09:00
tom5079
969e32e744 Dependency Update 2021-12-29 11:36:16 +09:00
tom5079
980909df9b Update README.md 2021-12-17 01:21:29 +09:00
tom5079
e6753088a4 User-Agent hack
Fixes unable to download some images
2021-12-13 10:46:57 +09:00
tom5079
cbdb6cb63a Update README.md 2021-12-12 20:08:20 +09:00
tom5079
3cdf1a899e Potential Image load fail fix 2021-12-12 20:06:23 +09:00
tom5079
c796be5de5 nvm 2021-11-24 16:05:11 +09:00
tom5079
db301cb0c3 Merge branch 'master' of github.com:tom5079/Pupil 2021-11-24 16:03:52 +09:00
tom5079
f00421ef23 state.jpg 2021-11-24 16:03:42 +09:00
tom5079
b324654967 Update README.md 2021-11-03 09:50:07 +09:00
tom5079
aa10ada3ee Dependency update 2021-11-03 09:42:12 +09:00
tom5079
10c97987fb Aligned with new hitomi.la image servers 2021-10-30 08:52:10 +09:00
tom5079
b532615bbd Aligned with new hitomi.la image servers 2021-10-29 16:55:12 +09:00
tom5079
3066f41af3 Update README.md 2021-10-28 08:33:35 +09:00
tom5079
0c401c6741 Merge remote-tracking branch 'origin/master' 2021-10-28 08:32:45 +09:00
tom5079
1a21d1c937 Aligned with new hitomi.la image servers 2021-10-28 08:30:59 +09:00
tom5079
525b49a5c9 Update README.md 2021-10-25 22:07:12 +09:00
tom5079
34c074bf7b Built APK 2021-10-25 09:33:25 +09:00
tom5079
b4dc961cdc Aligned with new hitomi.la image servers 2021-10-25 09:32:05 +09:00
tom5079
93374d2cfe Updated gradlew permission 2021-09-14 00:39:35 +09:00
tom5079
4009b10549 Align with hitomi image server 2021-08-11 22:22:35 +09:00
tom5079
db1864205f Merge remote-tracking branch 'origin/master' 2021-07-23 22:11:36 +09:00
tom5079
bf39ccabbd Fixed images not showing up 2021-07-23 22:11:28 +09:00
tom5079
0e8e7767ee Update README.md 2021-07-23 22:10:02 +09:00
tom5079
5b6c86e34f Fixed images not showing up 2021-07-23 22:07:18 +09:00
tom5079
6bbaca3686 Update README.md 2021-07-23 21:52:35 +09:00
tom5079
35eae90df1 Updated README.md 2021-07-23 21:51:38 +09:00
tom5079
488d43e076 Merge remote-tracking branch 'origin/master' 2021-07-23 21:50:25 +09:00
tom5079
7c5e93c171 Merge branch 'dev' 2021-07-23 21:49:18 +09:00
tom5079
a20ef783e1 Fixed thumbnail not visible 2021-07-23 21:36:41 +09:00
tom5079
8ae0dce0ed Update README.md 2021-07-10 12:36:42 +09:00
tom5079
44aea606b7 resigned apk 2021-07-09 18:22:53 +09:00
tom5079
a05dc8c661 Alignment with changed hitomi.la image server 2021-07-09 18:03:57 +09:00
tom5079
1f80e36017 Check update onResume() instead of onCreate() 2021-07-03 16:25:08 +09:00
tom5079
1efca40744 Dependency update & report savedset io exception 2021-07-03 16:22:52 +09:00
tom5079
86e3131afa Update README.md 2021-06-18 07:43:58 +09:00
tom5079
4910b4a4b0 Update README.md 2021-06-14 08:28:58 +09:00
tom5079
9c7320c0a0 Fix app crashing 2021-06-12 16:02:38 +09:00
tom5079
02c17c3b75 Potential fix for UpdateBroadcastReceiver 2021-06-12 15:47:23 +09:00
tom5079
49a47f4b4f 5.1.9-hotfix1 2021-06-08 20:05:16 +09:00
tom5079
68280f4a62 Update README.md 2021-06-08 20:02:03 +09:00
tom5079
0e3669b247 Update README.md 2021-06-08 14:03:02 +09:00
tom5079
4c9aa29d46 Fixed Downloaded folder showing up as not downloaded 2021-06-08 12:01:16 +09:00
tom5079
66fbf10f2d Update README.md 2021-06-08 09:19:49 +09:00
tom5079
15ad806eb8 Update README.md 2021-06-08 09:19:35 +09:00
tom5079
b7f80b9c82 5.1.9 2021-06-08 09:18:20 +09:00
tom5079
9b511d2f8f Fixed radio button acting up 2021-06-08 09:08:24 +09:00
tom5079
6ebce2deb3 Dependency update 2021-06-08 08:48:05 +09:00
tom5079
95dade13f4 Dependency update 2021-05-18 10:57:36 +09:00
tom5079
ba4449d003 Fixed Proxy dialog 2021-04-04 08:22:55 +09:00
tom5079
7632fe5e86 Dependency update 2021-02-18 10:03:51 +09:00
tom5079
2c56bcacee Dependency update & Apk build 2021-02-17 18:09:13 +09:00
tom5079
c8202db3c6 Merge remote-tracking branch 'origin/dev' into dev 2021-02-17 17:44:54 +09:00
tom5079
223d689b0c Merge pull request #115 from tom5079/issue-112
Pupil-112 [Feature request] Add the ability to manage the maximum parallel downloads
2021-02-17 17:44:32 +09:00
tom5079
4f0e7d9696 Dependency update 2021-02-17 17:44:14 +09:00
tom5079
f4ce911de9 Pupil-112 [Feature request] Add the ability to manage the maximum parallel downloads
Dependency update
2021-02-16 16:57:23 +09:00
tom5079
d0ad7effa0 Updated README.md 2021-02-13 18:14:18 +09:00
tom5079
a032beecbf Merge remote-tracking branch 'origin/master' 2021-02-13 18:13:52 +09:00
tom5079
46ec9e48d9 (Android 11) Show warning when the download folder is set to app internal space 2021-02-13 18:12:45 +09:00
tom5079
26bcef1cc0 Fixed Related gallery not showing up on GalleryDialog 2021-02-13 17:51:18 +09:00
tom5079
bfb2f44f8f Fixed favorite tag duplication 2021-02-13 17:43:44 +09:00
tom5079
28b19b6774 migration bug fixed 2021-01-13 09:49:49 +09:00
tom5079
8d72f4a3aa Update README.md 2021-01-12 12:55:50 +09:00
tom5079
9c62e0399d Update README.md 2021-01-12 12:43:09 +09:00
tom5079
65ea09854e Fixed Bug occuring on Android 11 2021-01-12 12:40:21 +09:00
tom5079
9f9a4c81b3 migrated to ViewBinding 2020-11-29 14:01:56 +09:00
tom5079
d567b30f4b Fixed typo & Built apk 2020-11-29 14:01:56 +09:00
tom5079
6d7c4ce0ab Fixed show extra tags button not showing up & version up 2020-11-29 14:01:51 +09:00
tom5079
e062b8f9e9 Implemented proper Page Turn without relying on RecyclerView 2020-11-29 14:01:46 +09:00
tom5079
08403b7a4e fixed gallery import 2020-11-29 14:01:41 +09:00
tom5079
c6ed5d35e7 ProgressCard 2020-11-29 14:01:25 +09:00
tom5079
dba3460b60 Fixes unable to recursively delete when the download folder is not not SAF based 2020-11-29 14:01:09 +09:00
tom5079
f07f624fcf search bug fix 2020-10-25 00:15:21 +09:00
tom5079
48ff2f328f search bug fix 2020-10-24 23:55:50 +09:00
tom5079
9ae2423a40 search bug fix 2020-10-24 23:05:11 +09:00
tom5079
2bc3c78c75 search bug fix 2020-10-24 23:04:49 +09:00
tom5079
18e9fe75fb hiyobi.me fix 2020-10-24 11:48:14 +09:00
tom5079
880a741a44 hiyobi.me fix 2020-10-24 11:25:16 +09:00
tom5079
2c6ddcc64b hitomi.la image not loading fix 2020-10-21 14:42:07 +09:00
tom5079
8f2e757b77 Update README.md 2020-10-15 15:53:30 +09:00
tom5079
ff177955b3 Update README.md 2020-10-15 15:49:07 +09:00
tom5079
8bb8066a98 Apk built 2020-10-15 14:37:13 +09:00
tom5079
2747ddbf65 Adjust gallery_id margin 2020-10-15 14:32:36 +09:00
tom5079
b939e9424d Translate tag by default 2020-10-15 14:29:22 +09:00
tom5079
fb9dea5d1e Copy galleryID by clicking galleryblock_id 2020-10-15 12:52:38 +09:00
tom5079
da4d5d711b Prefetch
Resolves #109
2020-10-15 10:20:36 +09:00
tom5079
331cbec5f1 Bug fix 2020-10-14 18:36:36 +09:00
tom5079
7f02284285 Update README.md 2020-10-14 00:26:06 +09:00
tom5079
ac2c3a6d97 Merge remote-tracking branch 'origin/master' into master 2020-10-14 00:25:44 +09:00
tom5079
c3bc80fec6 Bug fix 2020-10-14 00:24:38 +09:00
tom5079
09779a0710 Update README.md 2020-10-13 23:47:56 +09:00
tom5079
e82c6ef866 App built
Possible build time optimization
2020-10-13 23:40:53 +09:00
tom5079
861ae9be64 Merge remote-tracking branch 'origin/dev' into dev 2020-10-13 23:34:28 +09:00
tom5079
96108bc1ec Improves Scroll Jitter 2020-10-13 23:34:16 +09:00
tom5079
016f217db0 Merge pull request #108 from klx7007/patch-1
Fix FloatingSearchView imeOptions to only affect keyboard visibility
2020-10-13 23:05:34 +09:00
tom5079
0688294f18 Dependency update
Support non external storage document Uris

Support non external storage document Uris
2020-10-13 22:59:29 +09:00
klx7007
9ad008255d FloatingSearchView imeOptions 수정
imeOption을 덮어씌워서 search할때 키보드만 숨겨짐
2020-10-13 22:58:13 +09:00
tom5079
4c5a862dd6 Update README.md 2020-10-13 17:57:19 +09:00
tom5079
b165a2308f Merge remote-tracking branch 'origin/master' into master 2020-10-13 17:13:10 +09:00
tom5079
8757b08cd2 Fixed pagecount not showing up 2020-10-13 17:12:53 +09:00
tom5079
3800543fba Update README.md 2020-10-13 13:59:02 +09:00
tom5079
02ef60c818 Update README.md 2020-10-13 13:57:23 +09:00
tom5079
88f3b30266 Merge branch 'dev' into master 2020-10-13 13:52:34 +09:00
tom5079
9203dc0112 Tag translation 2020-10-13 13:51:53 +09:00
tom5079
4c683bec68 Dependency update
Fixes concurrentmodificationexception
2020-10-13 08:34:04 +09:00
tom5079
0cfd1eb453 Update README.md 2020-10-04 23:05:30 +09:00
tom5079
19744dab37 Merge remote-tracking branch 'origin/master' into master 2020-10-04 23:04:30 +09:00
tom5079
12d58e5aa7 Don't cancel download onPause 2020-10-04 23:04:12 +09:00
tom5079
e46dd37a26 Update README.md 2020-10-04 22:47:45 +09:00
103 changed files with 3612 additions and 2614 deletions

47
.gitignore vendored
View File

@@ -1,20 +1,33 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml *.iml
.gradle .idea/
/local.properties misc.xml
/.idea/caches deploymentTargetDropDown.xml
/.idea/libraries render.experimental.xml
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
#Github pages # Keystore files
/gh-pages *.jks
*.keystore
#Private files # Google Services (e.g. APIs or Firebase)
**/google-services.json google-services.json
**/credentials.json
# Android Profiling
*.hprof

View File

@@ -1,139 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Pupil, Hitomi.la viewer for Android&#10; Copyright (C) &amp;#36;today.year tom5079&#10;&#10; This program is free software: you can redistribute it and/or modify&#10; it under the terms of the GNU General Public License as published by&#10; the Free Software Foundation, either version 3 of the License, or&#10; (at your option) any later version.&#10;&#10; This program is distributed in the hope that it will be useful,&#10; but WITHOUT ANY WARRANTY; without even the implied warranty of&#10; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10; GNU General Public License for more details.&#10;&#10; You should have received a copy of the GNU General Public License&#10; along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;." />
<option name="myName" value="GPL" />
</copyright>
</component>

View File

@@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Pupil" copyright="GPL" />
</module2copyright>
</settings>
</component>

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="tom50">
<words>
<w>hitomi</w>
</words>
</dictionary>
</component>

4
.idea/encodings.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

20
.idea/gradle.xml generated
View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="http://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</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>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://guardian.github.com/maven/repo-releases" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://s3.amazonaws.com/fabric-artifacts-private/internal-snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://maven.fabric.io/public" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:/$USER_HOME$/.m2/repository" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://dl.bintray.com/tom5079/maven" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="http://dl.bintray.com/piasy/maven" />
</remote-repository>
</component>
</project>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinCodeInsightWorkspaceSettings">
<option name="addUnambiguousImportsOnTheFly" value="true" />
<option name="optimizeImportsOnTheFly" value="true" />
</component>
</project>

6
.idea/kotlinc.xml generated
View File

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

View File

@@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="Pupil" pattern="file[app]:*/" />
</component>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -2,8 +2,8 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total) ![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/latest/Pupil-v5.1.2-hotfix3.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.1.2-hotfix3/Pupil-v5.1.2-hotfix3.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.7/Pupil-v5.3.7.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.7/Pupil-v5.3.7.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features # Features
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.jpg?raw=true) ![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.jpg?raw=true)
@@ -20,4 +20,7 @@ or Build app yourself
# Contribution # Contribution
Any kind of contribution is appriciated. Feel free to leave PR! Any kind of contribution is appreciated. Feel free to leave PR!
## Tag Translation
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)

View File

@@ -1,11 +1,11 @@
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: "kotlin-android" apply plugin: "kotlin-android"
apply plugin: "kotlin-kapt" apply plugin: "kotlin-kapt"
apply plugin: "kotlin-android-extensions" apply plugin: "kotlin-parcelize"
apply plugin: "kotlinx-serialization" apply plugin: "kotlinx-serialization"
apply plugin: "com.google.android.gms.oss-licenses-plugin" apply plugin: "com.google.android.gms.oss-licenses-plugin"
if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) { if (file("google-services.json").exists()) {
logger.lifecycle("Firebase Enabled") logger.lifecycle("Firebase Enabled")
apply plugin: "com.google.gms.google-services" apply plugin: "com.google.gms.google-services"
apply plugin: "com.google.firebase.crashlytics" apply plugin: "com.google.firebase.crashlytics"
@@ -32,27 +32,27 @@ configurations {
} }
android { android {
compileSdkVersion 30 compileSdkVersion 32
defaultConfig { defaultConfig {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 32
versionCode 62 versionCode 69
versionName "5.1.3" versionName "5.3.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
debug { debug {
minifyEnabled true defaultConfig.minSdkVersion 21
shrinkResources true
minifyEnabled false
shrinkResources false
debuggable true debuggable true
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG" versionNameSuffix "-DEBUG"
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
ext.enableCrashlytics = false ext.enableCrashlytics = false
ext.alwaysUpdateBuildId = false ext.alwaysUpdateBuildId = false
} }
@@ -63,51 +63,56 @@ android {
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
} }
} }
buildFeatures {
viewBinding true
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental"
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_11
} }
buildToolsVersion = "29.0.3"
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0-RC2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.2.0-beta01" implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01" implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.preference:preference:1.1.1" implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.work:work-runtime-ktx:2.7.1"
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0-alpha03" implementation "com.google.android.material:material:1.5.0"
implementation "com.google.firebase:firebase-core:17.5.0" implementation platform('com.google.firebase:firebase-bom:29.0.3')
implementation "com.google.firebase:firebase-analytics:17.5.0" implementation "com.google.firebase:firebase-analytics-ktx"
implementation "com.google.firebase:firebase-crashlytics:17.2.2" implementation "com.google.firebase:firebase-crashlytics-ktx"
implementation "com.google.firebase:firebase-perf:19.0.9" implementation "com.google.firebase:firebase-perf-ktx"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0" implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1" implementation "com.google.android.gms:play-services-mlkit-face-detection:17.0.1"
implementation "com.github.clans:fab:1.6.4" implementation "com.github.clans:fab:1.6.4"
//implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1" //implementation "com.quiph.ui:recyclerviewfastscroller:0.2.1"
implementation 'com.github.piasy:BigImageViewer:1.6.7' implementation 'com.github.piasy:BigImageViewer:1.8.1'
implementation 'com.github.piasy:FrescoImageLoader:1.6.7' implementation 'com.github.piasy:FrescoImageLoader:1.8.1'
implementation 'com.github.piasy:FrescoImageViewFactory:1.6.7' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.1'
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.6.0'
//noinspection GradleDependency //noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
@@ -122,17 +127,15 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0" implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.7.2" implementation "org.jsoup:jsoup:1.14.3"
implementation "xyz.quaver:documentfilex:0.3.1"
implementation "xyz.quaver:floatingsearchview:1.0.7"
testImplementation "junit:junit:4.13" implementation "xyz.quaver:documentfilex:0.7.2"
androidTestImplementation "androidx.test.ext:junit:1.1.2" implementation "xyz.quaver:floatingsearchview:1.1.7"
androidTestImplementation "androidx.test:rules:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
}
androidExtensions { testImplementation "junit:junit:4.13.2"
experimental = true testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
} }

View File

@@ -32,4 +32,6 @@
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment -keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment -keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
-keep class xyz.quaver.pupil.** { *; }
-keep class app.cash.zipline.** { *; }

View File

@@ -1,5 +1,5 @@
{ {
"version": 1, "version": 3,
"artifactType": { "artifactType": {
"type": "APK", "type": "APK",
"kind": "Directory" "kind": "Directory"
@@ -10,11 +10,11 @@
{ {
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"properties": [], "attributes": [],
"versionCode": 62, "versionCode": 69,
"versionName": "5.1.3", "versionName": "5.3.8",
"enabled": true,
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
] ],
"elementType": "File"
} }

View File

@@ -20,10 +20,18 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.pupil.hitomi.*
import java.util.*
import java.util.concurrent.TimeUnit
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@@ -32,10 +40,144 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
// @Before
// fun init() {
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// }
@Before
fun init() {
clientBuilder = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.SECONDS)
.writeTimeout(0, TimeUnit.SECONDS)
.callTimeout(0, TimeUnit.SECONDS)
.connectTimeout(0, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("Referer", "https://hitomi.la/")
.build()
chain.proceed(request)
}
}
@Test @Test
fun useAppContext() { fun test_empty() {
// Context of the app under test. print(
val appContext = InstrumentationRegistry.getInstrumentation().targetContext "".trim()
.replace(Regex("""^\?"""), "")
.lowercase(Locale.getDefault())
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
})
} }
@Test
fun test_nozomi() {
val nozomi = getGalleryIDsFromNozomi(null, "index", "all")
Log.d("PUPILD", nozomi.size.toString())
}
@Test
fun test_search() {
val ids = getGalleryIDsForQuery("language:korean").reversed()
print(ids.size)
}
@Test
fun test_suggestions() {
val suggestions = getSuggestionsForQuery("language:g")
print(suggestions)
}
@Test
fun test_doSearch() {
val r = runBlocking {
doSearch("language:korean")
}
Log.d("PUPILD", r.take(10).toString())
}
// @Test
// fun test_getBlock() {
// val galleryBlock = getGalleryBlock(2097576)
//
// print(galleryBlock)
// }
//
// @Test
// fun test_getGallery() {
// val gallery = getGallery(2097751)
//
// print(gallery)
// }
@Test
fun test_getGalleryInfo() {
val info = getGalleryInfo(1469394)
print(info)
}
@Test
fun test_getReader() {
val reader = getGalleryInfo(2128654)
Log.d("PUPILD", reader.toString())
}
@Test
fun test_getImages() { runBlocking {
val galleryID = 2128654
val images = getGalleryInfo(galleryID).files.map {
imageUrlFromImage(galleryID, it,false)
}
Log.d("PUPILD", images.toString())
// images.forEachIndexed { index, image ->
// println("Testing $index/${images.size}: $image")
// val response = client.newCall(
// Request.Builder()
// .url(image)
// .header("Referer", "https://hitomi.la/")
// .build()
// ).execute()
//
// assertEquals(200, response.code())
//
// println("$index/${images.size} Passed")
// }
} }
// @Test
// fun test_urlFromUrlFromHash() {
// val url = urlFromUrlFromHash(1531795, GalleryFiles(
// 212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
// ), "webp")
//
// print(url)
// }
// @Test
// suspend fun test_doSearch_extreme() {
// val query = "language:korean -tag:sample -female:humiliation -female:diaper -female:strap-on -female:squirting -female:lizard_girl -female:voyeurism -type:artistcg -female:blood -female:ryona -male:blood -male:ryona -female:crotch_tattoo -male:urethra_insertion -female:living_clothes -male:tentacles -female:slave -female:gag -male:gag -female:wooden_horse -male:exhibitionism -male:miniguy -female:mind_break -male:mind_break -male:unbirth -tag:scanmark -tag:no_penetration -tag:nudity_only -female:enema -female:brain_fuck -female:navel_fuck -tag:novel -tag:mosaic_censorship -tag:webtoon -male:rape -female:rape -female:yuri -male:anal -female:anal -female:futanari -female:huge_breasts -female:big_areolae -male:torture -male:stuck_in_wall -female:stuck_in_wall -female:torture -female:birth -female:pregnant -female:drugs -female:bdsm -female:body_writing -female:cbt -male:dark_skin -male:insect -female:insect -male:vore -female:vore -female:vomit -female:urination -female:urethra_insertion -tag:mmf_threesome -female:sex_toys -female:double_penetration -female:eggs -female:prolapse -male:smell -male:bestiality -female:bestiality -female:big_ass -female:milf -female:mother -male:dilf -male:netorare -female:netorare -female:cosplaying -female:filming -female:armpit_sex -female:armpit_licking -female:tickling -female:lactation -male:skinsuit -female:skinsuit -male:bbm -female:prostitution -female:double_penetration -female:females_only -male:males_only -female:tentacles -female:tentacles -female:stomach_deformation -female:hairy_armpits -female:large_insertions -female:mind_control -male:orc -female:dark_skin -male:yandere -female:yandere -female:scat -female:toddlercon -female:bbw -female:hairy -male:cuntboy -male:lactation -male:drugs -female:body_modification -female:monoeye -female:chikan -female:long_tongue -female:harness -female:fisting -female:glory_hole -female:latex -male:latex -female:unbirth -female:giantess -female:sole_dickgirl -female:robot -female:doll_joints -female:machine -tag:artbook -male:cbt -female:farting -male:farting -male:midget -female:midget -female:exhibitionism -male:monster -female:big_nipples -female:big_clit -female:gyaru -female:piercing -female:necrophilia -female:snuff -female:smell -male:cheating -female:cheating -male:snuff -female:harem -male:harem"
// print(doSearch(query).size)
// }
// @Test
// suspend fun test_parse() {
// print(doSearch("-male:yaoi -female:yaoi -female:loli").size)
// }
// @Test
// fun test_subdomainFromUrl() {
// val galleryInfo = getGalleryInfo(1929109).files[2]
// print(urlFromUrlFromHash(1929109, galleryInfo, "webp", null, "a"))
// }
} }

View File

@@ -6,10 +6,11 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@@ -58,7 +59,8 @@
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:parentActivityName=".ui.MainActivity"> android:parentActivityName=".ui.MainActivity"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -222,7 +224,8 @@
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/NoActionBarAppTheme"> android:theme="@style/NoActionBarAppTheme"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View File

@@ -18,31 +18,40 @@
package xyz.quaver.pupil package xyz.quaver.pupil
import android.app.* import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.fresco.FrescoImageLoader import com.github.piasy.biv.loader.fresco.FrescoImageLoader
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.*
import okhttp3.Dispatcher
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.pupil.hitomi.evaluationContext
import xyz.quaver.pupil.hitomi.readText
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.setClient
import java.io.File import java.io.File
import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -65,12 +74,17 @@ var clientHolder: OkHttpClient? = null
val client: OkHttpClient val client: OkHttpClient
get() = clientHolder ?: clientBuilder.build().also { get() = clientHolder ?: clientBuilder.build().also {
clientHolder = it clientHolder = it
setClient(it)
} }
class Pupil : Application() { class Pupil : Application() {
companion object {
lateinit var instance: Pupil
private set
}
override fun onCreate() { override fun onCreate() {
instance = this
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
preferences = PreferenceManager.getDefaultSharedPreferences(this) preferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -80,24 +94,34 @@ class Pupil : Application() {
else userID else userID
} }
FirebaseApp.initializeApp(this)
FirebaseCrashlytics.getInstance().setUserId(userID) FirebaseCrashlytics.getInstance().setUserId(userID)
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()
clientBuilder = OkHttpClient.Builder() clientBuilder = OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS) // .connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS)
.proxyInfo(proxyInfo) .proxyInfo(proxyInfo)
.addInterceptor { chain -> .addInterceptor { chain ->
val request = chain.request() val request = chain.request().newBuilder()
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
.header("Referer", "https://hitomi.la/")
.build()
val tag = request.tag() ?: return@addInterceptor chain.proceed(request) val tag = request.tag() ?: return@addInterceptor chain.proceed(request)
interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request) interceptors[tag::class]?.invoke(chain) ?: chain.proceed(request)
}.apply {
(Preferences.get<String>("max_concurrent_download").toIntOrNull() ?: 0).let {
if (it != 0)
dispatcher(Dispatcher(Executors.newFixedThreadPool(it)))
}
} }
try { try {
Preferences.get<String>("download_folder").also { Preferences.get<String>("download_folder").also {
if (it.startsWith("content") && Build.VERSION.SDK_INT > 19) if (it.startsWith("content://"))
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
Uri.parse(it), Uri.parse(it),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -105,8 +129,6 @@ class Pupil : Application() {
if (!FileX(this, it).canWrite()) if (!FileX(this, it).canWrite())
throw Exception() throw Exception()
DownloadManager.getInstance(this).migrate()
} }
} catch (e: Exception) { } catch (e: Exception) {
Preferences.remove("download_folder") Preferences.remove("download_folder")
@@ -122,8 +144,13 @@ class Pupil : Application() {
favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse("")) favoriteTags = SavedSet(File(ContextCompat.getDataDir(this), "favorites_tags.json"), Tag.parse(""))
searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "") searchHistory = SavedSet(File(ContextCompat.getDataDir(this), "search_histories.json"), "")
favoriteTags.filter { it.tag.contains('_') }.forEach {
favoriteTags.remove(it)
}
/*
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false) FirebaseAnalytics.getInstance(this).setAnalyticsCollectionEnabled(false)*/
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
@@ -133,7 +160,14 @@ class Pupil : Application() {
e.printStackTrace() e.printStackTrace()
} }
BigImageViewer.initialize(FrescoImageLoader.with(this)) BigImageViewer.initialize(
FrescoImageLoader.with(
this,
OkHttpImagePipelineConfigFactory
.newBuilder(this, client)
.build()
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

View File

@@ -18,14 +18,14 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
@@ -34,15 +34,16 @@ import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.hitomi.getGallery
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.GalleryblockItemBinding
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.hitomi.getGallery
import xyz.quaver.pupil.hitomi.getGalleryInfo
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
@@ -51,327 +52,260 @@ import java.io.File
class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface { class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType {
NEXT,
GALLERY,
PREV
}
var updateAll = true var updateAll = true
var thin: Boolean = Preferences["thin"] var thin: Boolean = Preferences["thin"]
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val binding: GalleryblockItemBinding) : RecyclerView.ViewHolder(binding.root) {
private var galleryID: Int = 0 private var galleryID: Int = 0
init { init {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
while (updateAll) { while (updateAll) {
updateProgress(view.context) updateProgress(itemView.context)
delay(1000) delay(1000)
} }
} }
} }
private fun updateProgress(context: Context) { private fun updateProgress(context: Context) = CoroutineScope(Dispatchers.Main).launch {
val cache = Cache.getInstance(context, galleryID) with(binding.galleryblockCard) {
val imageList = Cache.getInstance(context, galleryID).metadata.imageList
CoroutineScope(Dispatchers.Main).launch { if (imageList == null) {
if (cache.metadata.reader == null) { max = 0
view.galleryblock_progressbar_layout.visibility = View.GONE return@with
view.galleryblock_progress_complete.visibility = View.INVISIBLE
return@launch
} }
with(view.galleryblock_progressbar) { progress = imageList.count { it != null }
val imageList = cache.metadata.imageList!! max = imageList.size
progress = imageList.count { it != null } this@GalleryViewHolder.binding.galleryblockId.setOnClickListener {
max = imageList.size (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("gallery_id", galleryID.toString())
with(view.galleryblock_progressbar_layout) { )
if (visibility == View.GONE) Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
visibility = View.VISIBLE
}
if (!imageList.contains(null)) {
val downloadManager = DownloadManager.getInstance(context)
if (completeFlag.get(galleryID, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(
if (downloadManager.getDownloadFolder(galleryID) != null)
R.drawable.ic_progressbar
else R.drawable.ic_progressbar_cache
)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context,
if (downloadManager.getDownloadFolder(galleryID) != null)
R.drawable.ic_progressbar_complete
else R.drawable.ic_progressbar_complete_cache
).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryID, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
} }
type = if (!imageList.contains(null)) {
val downloadManager = DownloadManager.getInstance(context)
if (downloadManager.getDownloadFolder(galleryID) == null)
ProgressCard.Type.CACHE
else
ProgressCard.Type.DOWNLOAD
} else
ProgressCard.Type.LOADING
} }
} }
fun bind(galleryID: Int) { fun bind(galleryID: Int) {
this.galleryID = galleryID this.galleryID = galleryID
updateProgress(itemView.context)
val cache = Cache.getInstance(view.context, galleryID) val cache = Cache.getInstance(itemView.context, galleryID)
val galleryBlock = runBlocking { CoroutineScope(Dispatchers.IO).launch {
cache.getGalleryBlock() val galleryBlock = cache.getGalleryBlock() ?: return@launch
} ?: return
with(view) { launch(Dispatchers.Main) {
val resources = context.resources val resources = itemView.context.resources
val languages = resources.getStringArray(R.array.languages).map { val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split -> it.split("|").let { split ->
Pair(split[0], split[1]) Pair(split[0], split[1])
}
}.toMap()
val artists = galleryBlock.artists
val series = galleryBlock.series
binding.galleryblockThumbnail.apply {
setOnClickListener {
itemView.performClick()
}
setOnLongClickListener {
itemView.performLongClick()
}
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
setImageLoaderCallback(object: ImageLoader.Callback {
override fun onFail(error: Exception?) {
Cache.delete(context, galleryID)
}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
}
} }
}.toMap()
val artists = galleryBlock.artists binding.galleryblockTitle.text = galleryBlock.title
val series = galleryBlock.series with(binding.galleryblockArtist) {
text = artists.joinToString { it.wordCapitalize() }
visibility = when {
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
galleryblock_thumbnail.apply { CoroutineScope(Dispatchers.IO).launch {
setOnClickListener { val gallery = runCatching {
view.performClick() getGallery(galleryID)
}.getOrNull()
if (gallery?.groups?.isNotEmpty() != true)
return@launch
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
} }
setOnLongClickListener { with(binding.galleryblockSeries) {
view.performLongClick() text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
} }
setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant)) binding.galleryblockType.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
setImageLoaderCallback(object: ImageLoader.Callback { with(binding.galleryblockLanguage) {
override fun onFail(error: Exception?) { text =
Cache.getInstance(context, galleryID).let { cache -> resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
cache.cacheFolder.getChild(".thumbnail").let { if (it.exists()) it.delete() } visibility = when {
cache.downloadFolder?.getChild(".thumbnail")?.let { if (it.exists()) it.delete() } !galleryBlock.language.isNullOrEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(binding.galleryblockTagGroup) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
} }
} }
override fun onCacheHit(imageType: Int, image: File?) {} tags.clear()
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
override fun onStart() {}
override fun onSuccess(image: File?) {}
})
ssiv?.recycle()
CoroutineScope(Dispatchers.IO).launch {
cache.getThumbnail().let { launch(Dispatchers.Main) {
showImage(it)
} }
}
}
galleryblock_title.text = galleryBlock.title CoroutineScope(Dispatchers.IO).launch {
with(galleryblock_artist) { tags.addAll(
text = artists.joinToString { it.wordCapitalize() } galleryBlock.relatedTags.sortedBy {
visibility = when { val tag = Tag.parse(it)
artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
CoroutineScope(Dispatchers.IO).launch { if (favoriteTags.contains(tag))
val gallery = runCatching { -1
getGallery(galleryID) else
}.getOrNull() when(Tag.parse(it).area) {
"female" -> 0
if (gallery?.groups?.isNotEmpty() != true) "male" -> 1
return@launch else -> 2
launch(Dispatchers.Main) {
text = context.getString(
R.string.galleryblock_artist_with_group,
artists.joinToString { it.wordCapitalize() },
gallery.groups.joinToString { it.wordCapitalize() }
)
}
}
}
with(galleryblock_series) {
text =
resources.getString(
R.string.galleryblock_series,
series.joinToString(", ") { it.wordCapitalize() })
visibility = when {
series.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(galleryblock_language) {
text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
visibility = when {
galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE
}
}
with(galleryblock_tag_group) {
onClickListener = {
onChipClickedHandler.forEach { callback ->
callback.invoke(it)
}
}
tags.clear()
tags.addAll(
galleryBlock.relatedTags.sortedBy {
val tag = Tag.parse(it)
if (favoriteTags.contains(tag))
-1
else
when(Tag.parse(it).area) {
"female" -> 0
"male" -> 1
else -> 2
}
}.map {
Tag.parse(it)
}
)
refresh()
}
galleryblock_id.text = galleryBlock.id.toString()
galleryblock_pagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getReader(galleryBlock.id).galleryInfo.files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
galleryblock_pagecount.text = context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(galleryblock_favorite) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
}
else -> {
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
} }
}) }.map {
start() Tag.parse(it)
}) }
)
launch(Dispatchers.Main) {
refresh()
}
}
}
binding.galleryblockId.text = galleryBlock.id.toString()
binding.galleryblockPagecount.text = "-"
CoroutineScope(Dispatchers.IO).launch {
val pageCount = kotlin.runCatching {
getGalleryInfo(galleryBlock.id).files.size
}.getOrNull() ?: return@launch
withContext(Dispatchers.Main) {
binding.galleryblockPagecount.text = itemView.context.getString(R.string.galleryblock_pagecount, pageCount)
}
}
with(binding.galleryblockFavorite) {
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
setOnClickListener {
when {
favorites.contains(galleryBlock.id) -> {
favorites.remove(galleryBlock.id)
setImageResource(R.drawable.ic_star_empty)
}
else -> {
favorites.add(galleryBlock.id)
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
this ?: return@apply
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
setImageResource(R.drawable.ic_star_filled)
}
})
start()
})
}
} }
} }
} }
}
// Make some views invisible to make it thinner
if (thin) {
galleryblock_tag_group.visibility = View.GONE
} }
} }
// Make some views invisible to make it thinner
if (thin) {
binding.galleryblockTagGroup.visibility = View.GONE
}
} }
} }
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
class PrevViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
class ViewHolderFactory {
companion object {
fun getLayoutID(type: Int): Int {
return when(ViewType.values()[type]) {
ViewType.NEXT -> R.layout.item_next
ViewType.PREV -> R.layout.item_prev
ViewType.GALLERY -> R.layout.item_galleryblock
}
}
}
}
val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((Int) -> Unit)? = null var onDownloadClickedHandler: ((Int) -> Unit)? = null
var onDeleteClickedHandler: ((Int) -> Unit)? = null var onDeleteClickedHandler: ((Int) -> Unit)? = null
var showNext = false
var showPrev = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return GalleryViewHolder(GalleryblockItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
fun getViewHolder(type: Int, view: View): RecyclerView.ViewHolder {
return when(ViewType.values()[type]) {
ViewType.NEXT -> NextViewHolder(view as LinearLayout)
ViewType.PREV -> PrevViewHolder(view as LinearLayout)
ViewType.GALLERY -> GalleryViewHolder(view as CardView)
}
}
return getViewHolder(
viewType,
LayoutInflater.from(parent.context).inflate(
ViewHolderFactory.getLayoutID(viewType),
parent,
false
)
)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) { if (holder is GalleryViewHolder) {
val galleryID = galleries[position-(if (showPrev) 1 else 0)] val galleryID = galleries[position]
holder.bind(galleryID) holder.bind(galleryID)
with(holder.view.galleryblock_primary) { holder.binding.galleryblockCard.binding.download.setOnClickListener {
setOnClickListener {
holder.view.performClick()
}
setOnLongClickListener {
holder.view.performLongClick()
}
}
holder.view.galleryblock_download.setOnClickListener {
onDownloadClickedHandler?.invoke(position) onDownloadClickedHandler?.invoke(position)
} }
holder.view.galleryblock_delete.setOnClickListener { holder.binding.galleryblockCard.binding.delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position) onDeleteClickedHandler?.invoke(position)
} }
mItemManger.bindView(holder.view, position) mItemManger.bindView(holder.binding.root, position)
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener { holder.binding.galleryblockCard.binding.swipeLayout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) { override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout) mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = holder.binding.galleryblockCard.binding.download.text =
if (DownloadManager.getInstance(holder.view.context).isDownloading(galleryID)) if (DownloadManager.getInstance(holder.binding.root.context).isDownloading(galleryID))
holder.view.context.getString(android.R.string.cancel) holder.binding.root.context.getString(android.R.string.cancel)
else else
holder.view.context.getString(R.string.main_download) holder.binding.root.context.getString(R.string.main_download)
} }
override fun onClose(layout: SwipeLayout?) {} override fun onClose(layout: SwipeLayout?) {}
@@ -383,18 +317,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
} }
} }
override fun getItemCount() = override fun getItemCount() = galleries.size
galleries.size +
(if (showNext) 1 else 0) +
(if (showPrev) 1 else 0)
override fun getItemViewType(position: Int): Int { override fun getSwipeLayoutResourceId(position: Int) = R.id.swipe_layout
return when {
showPrev && position == 0 -> ViewType.PREV
showNext && position == galleries.size+(if (showPrev) 1 else 0) -> ViewType.NEXT
else -> ViewType.GALLERY
}.ordinal
}
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
} }

View File

@@ -1,86 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_mirrors.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.Preferences
import java.util.*
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
it.split('|').let { split ->
Pair(split.first(), split.last())
}
}.toMap()
val list = mirrors.keys.toMutableList().apply {
Preferences.get<String>("mirrors")
.split(">")
.reversed()
.forEach {
if (this.contains(it)) {
this.remove(it)
this.add(0, it)
}
}
}
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
Collections.swap(list, from, to)
notifyItemMoved(from, to)
onItemMoved?.invoke(list)
}
var onStartDrag : ((ViewHolder) -> Unit)? = null
var onItemMoved : ((List<String>) -> (Unit))? = null
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.view) {
mirror_name.text = mirrors[list.elementAt(position)]
mirror_button.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
onStartDrag?.invoke(holder)
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate(
R.layout.item_mirrors, parent, false
).let {
ViewHolder(it)
}
}
override fun getItemCount() = mirrors.size
}

View File

@@ -19,7 +19,6 @@
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.content.Context import android.content.Context
import android.graphics.DiscretePathEffect
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -40,10 +39,10 @@ import com.facebook.imagepipeline.image.ImageInfo
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import com.github.piasy.biv.view.ImageShownCallback import com.github.piasy.biv.view.ImageShownCallback
import com.github.piasy.biv.view.ImageViewFactory import com.github.piasy.biv.view.ImageViewFactory
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ReaderItemBinding
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import java.io.File import java.io.File
@@ -53,16 +52,93 @@ class ReaderAdapter(
private val activity: ReaderActivity, private val activity: ReaderActivity,
private val galleryID: Int private val galleryID: Int
) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var galleryInfo: GalleryInfo? = null
var reader: Reader? = null
var isFullScreen = false var isFullScreen = false
var onItemClickListener : (() -> (Unit))? = null var onItemClickListener : (() -> (Unit))? = null
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class ViewHolder(private val binding: ReaderItemBinding) : RecyclerView.ViewHolder(binding.root) {
init {
with (binding.image) {
setImageViewFactory(FrescoImageViewFactory().apply {
updateView = { imageInfo ->
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
}
}
})
setImageShownCallback(object : ImageShownCallback {
override fun onMainImageShown() {
binding.image.mainView.let { v ->
when (v) {
is SubsamplingScaleImageView ->
if (!isFullScreen) binding.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
}
override fun onThumbnailShown() {}
})
setFailureImage(ContextCompat.getDrawable(itemView.context, R.drawable.image_broken_variant))
setOnClickListener {
onItemClickListener?.invoke()
}
}
binding.root.setOnClickListener {
onItemClickListener?.invoke()
}
}
fun bind(position: Int) {
if (cache == null)
cache = Cache.getInstance(itemView.context, galleryID)
if (!isFullScreen) {
binding.root.setBackgroundResource(R.drawable.reader_item_boundary)
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0
dimensionRatio =
"${galleryInfo!!.files[position].width}:${galleryInfo!!.files[position].height}"
}
} else {
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_PARENT
dimensionRatio = null
}
binding.root.background = null
}
binding.readerIndex.text = (position+1).toString()
val image = cache!!.getImage(position)
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
if (progress?.isInfinite() == true && image != null) {
binding.progressGroup.visibility = View.INVISIBLE
binding.image.showImage(image.uri)
} else {
binding.progressGroup.visibility = View.VISIBLE
binding.readerItemProgressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
clear()
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
notifyItemChanged(position)
}
}
}
fun clear() { fun clear() {
view.image.mainView.let { binding.image.mainView.let {
when (it) { when (it) {
is SubsamplingScaleImageView -> is SubsamplingScaleImageView ->
it.recycle() it.recycle()
@@ -74,91 +150,15 @@ class ReaderAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return LayoutInflater.from(parent.context).inflate( return ViewHolder(ReaderItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
R.layout.item_reader, parent, false
).let {
with(it) {
image.setImageViewFactory(FrescoImageViewFactory().apply {
updateView = { imageInfo ->
it.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
dimensionRatio = "${imageInfo.width}:${imageInfo.height}"
}
}
})
image.setImageShownCallback(object : ImageShownCallback {
override fun onMainImageShown() {
it.image.mainView.let { v ->
when (v) {
is SubsamplingScaleImageView ->
if (!isFullScreen) it.image.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
}
}
override fun onThumbnailShown() {}
})
image.setFailureImage(ContextCompat.getDrawable(context, R.drawable.image_broken_variant))
image.setOnClickListener {
this.performClick()
}
setOnClickListener {
onItemClickListener?.invoke()
}
}
ViewHolder(it)
}
} }
private var cache: Cache? = null private var cache: Cache? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout holder.bind(position)
if (cache == null)
cache = Cache.getInstance(holder.view.context, galleryID)
if (!isFullScreen) {
holder.view.setBackgroundResource(R.drawable.reader_item_boundary)
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = 0
dimensionRatio =
"${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
}
} else {
holder.view.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
holder.view.image.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_PARENT
dimensionRatio = null
}
holder.view.background = null
}
holder.view.reader_index.text = (position+1).toString()
val image = cache!!.getImage(position)
val progress = activity.downloader?.progress?.get(galleryID)?.get(position)
if (progress?.isInfinite() == true && image != null) {
holder.view.progress_group.visibility = View.INVISIBLE
holder.view.image.showImage(image.uri)
} else {
holder.view.progress_group.visibility = View.VISIBLE
holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
holder.clear()
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
notifyItemChanged(position)
}
}
} }
override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0 override fun getItemCount() = galleryInfo?.files?.size ?: 0
override fun onViewRecycled(holder: ViewHolder) { override fun onViewRecycled(holder: ViewHolder) {
holder.clear() holder.clear()

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock.System.now
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import xyz.quaver.pupil.client
import java.io.IOException
import java.net.URL
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime
const val protocol = "https:"
@Serializable
data class Artist(
val artist: String,
val url: String
)
@Serializable
data class Group(
val group: String,
val url: String
)
@Serializable
data class Parody(
val parody: String,
val url: String
)
@Serializable
data class Character(
val character: String,
val url: String
)
@Serializable
data class Tag(
val tag: String,
val url: String,
val female: String? = null,
val male: String? = null
)
@Serializable
data class Language(
val galleryid: String,
val url: String,
val language_localname: String,
val name: String
)
@Serializable
data class GalleryInfo(
val id: String,
val title: String,
val japanese_title: String? = null,
val language: String? = null,
val type: String,
val date: String,
val artists: List<Artist>? = null,
val groups: List<Group>? = null,
val parodys: List<Parody>? = null,
val tags: List<Tag>? = null,
val related: List<Int> = emptyList(),
val languages: List<Language> = emptyList(),
val characters: List<Character>? = null,
val scene_indexes: List<Int>? = emptyList(),
val files: List<GalleryFiles> = emptyList()
)
val json = Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
}
typealias HeaderSetter = (Request.Builder) -> Request.Builder
fun URL.readText(settings: HeaderSetter? = null): String {
val request = Request.Builder()
.url(this).let {
settings?.invoke(it) ?: it
}.build()
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.string() } ?: throw IOException()
}
fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
val request = Request.Builder()
.url(this).let {
settings?.invoke(it) ?: it
}.build()
return client.newCall(request).execute().also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw IOException()
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun getGalleryInfo(galleryID: Int) =
json.decodeFromString<GalleryInfo>(
URL("$protocol//$domain/galleries/$galleryID.js").readText()
.replace("var galleryinfo = ", "")
)
//common.js
const val domain = "ltn.hitomi.la"
const val galleryblockextension = ".html"
const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi"
val evaluationContext = Dispatchers.Main + Job()
object gg {
private var lastRetrieval: Long? = null
private val mutex = Mutex()
private var mDefault = 0
private val mMap = mutableMapOf<Int, Int>()
private var b = ""
@OptIn(ExperimentalTime::class, ExperimentalCoroutinesApi::class)
private suspend fun refresh() = withContext(Dispatchers.IO) {
mutex.withLock {
if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) {
val ggjs: String = suspendCancellableCoroutine { continuation ->
val call = client.newCall(Request.Builder().url("https://ltn.hitomi.la/gg.js").build())
call.enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
if (!call.isCanceled) {
response.body()?.use {
continuation.resume(it.string()) {
call.cancel()
}
}
}
}
})
continuation.invokeOnCancellation {
call.cancel()
}
}
mDefault = Regex("var o = (\\d)").find(ggjs)!!.groupValues[1].toInt()
val o = Regex("o = (\\d); break;").find(ggjs)!!.groupValues[1].toInt()
mMap.clear()
Regex("case (\\d+):").findAll(ggjs).forEach {
val case = it.groupValues[1].toInt()
mMap[case] = o
}
b = Regex("b: '(.+)'").find(ggjs)!!.groupValues[1]
lastRetrieval = System.currentTimeMillis()
}
}
}
suspend fun m(g: Int): Int {
refresh()
return mMap[g] ?: mDefault
}
suspend fun b(): String {
refresh()
return b
}
fun s(h: String): String {
val m = Regex("(..)(.)$").find(h)
return m!!.groupValues.let { it[2]+it[1] }.toInt(16).toString(10)
}
}
suspend fun subdomainFromURL(url: String, base: String? = null) : String {
var retval = "b"
if (!base.isNullOrBlank())
retval = base
val b = 16
val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
val m = r.find(url) ?: return "a"
val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b)
if (g != null) {
retval = (97+ gg.m(g)).toChar().toString() + retval
}
return retval
}
suspend fun urlFromUrl(url: String, base: String? = null) : String {
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
}
suspend fun fullPathFromHash(hash: String) : String =
"${gg.b()}${gg.s(hash)}/$hash"
fun realFullPathFromHash(hash: String): String =
hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash")
suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' }
val dir = dir ?: "images"
return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
}
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
if (base == "tn")
urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base)
else
urlFromUrl(urlFromHash(galleryID, image, dir, ext), base)
suspend fun rewriteTnPaths(html: String) {
html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")) { url ->
runBlocking {
urlFromUrl(url.value, "tn")
}
}
}
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return urlFromUrlFromHash(galleryID, image, "webp", null, "a")
// return when {
// noWebp ->
// urlFromUrlFromHash(galleryID, image)
//// image.hasavif != 0 ->
//// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
// image.haswebp != 0 ->
// urlFromUrlFromHash(galleryID, image, "webp", null, "a")
// else ->
// urlFromUrlFromHash(galleryID, image)
// }
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
@Serializable
data class Gallery(
val related: List<Int>,
val langList: List<Pair<String, String>>,
val cover: String,
val title: String,
val artists: List<String>,
val groups: List<String>,
val type: String,
val language: String,
val series: List<String>,
val characters: List<String>,
val tags: List<String>,
val thumbnails: List<String>
)
suspend fun getGallery(galleryID: Int) : Gallery {
val info = getGalleryInfo(galleryID)
return Gallery(
info.related,
info.languages.map { it.name to it.galleryid },
urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn"),
info.title,
info.artists?.map { it.artist }.orEmpty(),
info.groups?.map { it.group }.orEmpty(),
info.type,
info.language.orEmpty(),
info.parodys?.map { it.parody }.orEmpty(),
info.characters?.map { it.character }.orEmpty(),
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
info.files.map { urlFromUrlFromHash(galleryID, it, "webpsmalltn", "webp", "tn") }
)
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.net.URL
import java.net.URLDecoder
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.net.ssl.HttpsURLConnection
import kotlin.io.readText
//galleryblock.js
fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair<List<Int>, Int> {
val url = when(area) {
null -> "$protocol//$domain/$tag-$language$nozomiextension"
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
}
with(URL(url).openConnection() as HttpsURLConnection) {
requestMethod = "GET"
if (start != -1 && count != -1) {
val startByte = start*4
val endByte = (start+count)*4-1
setRequestProperty("Range", "bytes=$startByte-$endByte")
}
connect()
val totalItems = getHeaderField("Content-Range")
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
val nozomi = ArrayList<Int>()
val arrayBuffer = ByteBuffer
.wrap(inputStream.readBytes())
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return Pair(nozomi, totalItems)
}
}
@Serializable
data class GalleryBlock(
val id: Int,
val galleryUrl: String,
val thumbnails: List<String>,
val title: String,
val artists: List<String>,
val series: List<String>,
val type: String,
val language: String,
val relatedTags: List<String>,
val groups: List<String> = emptyList()
)
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {
val info = getGalleryInfo(galleryID)
return GalleryBlock(
galleryID,
"",
listOf(urlFromUrlFromHash(galleryID, info.files.first(), "webpbigtn", "webp", "tn")),
info.title,
info.artists?.map { it.artist }.orEmpty(),
info.parodys?.map { it.parody }.orEmpty(),
info.type,
info.language.orEmpty(),
info.tags?.map { "${if (it.female.isNullOrEmpty()) "" else "female:"}${if (it.male.isNullOrEmpty()) "" else "male:"}${it.tag}" }.orEmpty(),
info.groups?.map { it.group }.orEmpty()
)
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import kotlinx.serialization.Serializable
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getGalleryInfo
@Serializable
data class GalleryFiles(
val width: Int,
val hash: String,
val haswebp: Int = 0,
val name: String,
val height: Int,
val hasavif: Int = 0,
val hasavifsmalltn: Int? = 0
)
//Set header `Referer` to reader url to avoid 403 error
@Deprecated("", replaceWith = ReplaceWith("getGalleryInfo"))
fun getReader(galleryID: Int) : GalleryInfo {
return getGalleryInfo(galleryID)
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.*
suspend fun doSearch(query: String, sortByPopularity: Boolean = false) : Set<Int> = coroutineScope {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
for (term in terms) {
if (term.matches(Regex("^-.+")))
negativeTerms.push(term.replace(Regex("^-"), ""))
else if (term.isNotBlank())
positiveTerms.push(term)
}
val positiveResults = positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it)
}.getOrElse { emptySet() }
}
}
val negativeResults = negativeTerms.mapIndexed { index, it ->
async {
runCatching {
getGalleryIDsForQuery(it)
}.getOrElse { emptySet() }
}
}
val results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
else -> emptySet()
}.toMutableSet()
fun filterPositive(newResults: Set<Int>) {
when {
results.isEmpty() -> results.addAll(newResults)
else -> results.retainAll(newResults)
}
}
fun filterNegative(newResults: Set<Int>) {
results.removeAll(newResults)
}
//positive results
positiveResults.forEach {
filterPositive(it.await())
}
//negative results
negativeResults.forEachIndexed { index, it ->
filterNegative(it.await())
}
results
}

View File

@@ -0,0 +1,328 @@
/*
* Copyright 2019 tom5079
*
* 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
*
* http://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 xyz.quaver.pupil.hitomi
import okhttp3.Request
import xyz.quaver.pupil.client
import java.net.URL
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import kotlin.math.min
//searchlib.js
const val separator = "-"
const val extension = ".html"
const val index_dir = "tagindex"
const val galleries_index_dir = "galleriesindex"
const val max_node_size = 464
const val B = 16
const val compressed_nozomi_prefix = "n"
val tag_index_version: String by lazy { getIndexVersion("tagindex") }
val galleries_index_version: String by lazy { getIndexVersion("galleriesindex") }
fun sha256(data: ByteArray) : ByteArray {
return MessageDigest.getInstance("SHA-256").digest(data)
}
@OptIn(ExperimentalUnsignedTypes::class)
fun hashTerm(term: String) : UByteArray {
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
}
fun sanitize(input: String) : String {
return input.replace(Regex("[/#]"), "")
}
fun getIndexVersion(name: String) =
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").readText()
//search.js
fun getGalleryIDsForQuery(query: String) : Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
val sides = it.split(":")
val ns = sides[0]
var tag = sides[1]
var area : String? = ns
var language = "all"
when (ns) {
"female", "male" -> {
area = "tag"
tag = it
}
"language" -> {
area = null
language = tag
tag = "index"
}
}
return getGalleryIDsFromNozomi(area, tag, language)
}
val key = hashTerm(it)
val field = "galleries"
val node = getNodeAtAddress(field, 0) ?: return emptySet()
val data = bSearch(field, key, node)
if (data != null)
return getGalleryIDsFromData(data)
return emptySet()
}
}
fun getSuggestionsForQuery(query: String) : List<Suggestion> {
query.replace('_', ' ').let {
var field = "global"
var term = it
if (term.indexOf(':') > -1) {
val sides = it.split(':')
field = sides[0]
term = sides[1]
}
val key = hashTerm(term)
val node = getNodeAtAddress(field, 0) ?: return emptyList()
val data = bSearch(field, key, node)
if (data != null)
return getSuggestionsFromData(field, data)
return emptyList()
}
}
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
val (offset, length) = data
if (length > 10000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length))
val suggestions = ArrayList<Suggestion>()
val buffer = ByteBuffer
.wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfSuggestions = buffer.int
if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
throw Exception("number of suggestions $numberOfSuggestions is too long")
for (i in 0.until(numberOfSuggestions)) {
var top = buffer.int
val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
buffer.position(buffer.position()+top)
top = buffer.int
val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
buffer.position(buffer.position()+top)
val count = buffer.int
val tagname = sanitize(tag)
val u =
when(ns) {
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
"language" -> "/index-$tagname${separator}1$extension"
else -> "/$ns/$tagname${separator}all${separator}1$extension"
}
suggestions.add(Suggestion(tag, count, u, ns))
}
return suggestions
}
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set<Int> {
val nozomiAddress =
when(area) {
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
}
val bytes = try {
URL(nozomiAddress).readBytes()
} catch (e: Exception) {
return emptySet()
}
val nozomi = mutableSetOf<Int>()
val arrayBuffer = ByteBuffer
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int)
return nozomi
}
fun getGalleryIDsFromData(data: Pair<Long, Int>) : Set<Int> {
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
val (offset, length) = data
if (length > 100000000 || length <= 0)
throw Exception("length $length is too long")
val inbuf = getURLAtRange(url, offset.until(offset+length))
val galleryIDs = mutableSetOf<Int>()
val buffer = ByteBuffer
.wrap(inbuf)
.order(ByteOrder.BIG_ENDIAN)
val numberOfGalleryIDs = buffer.int
val expectedLength = numberOfGalleryIDs*4+4
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
else if (inbuf.size != expectedLength)
throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
for (i in 0.until(numberOfGalleryIDs))
galleryIDs.add(buffer.int)
return galleryIDs
}
fun getNodeAtAddress(field: String, address: Long) : Node? {
val url =
when(field) {
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
"languages" -> "$protocol//$domain/$galleries_index_dir/languages.$galleries_index_version.index"
"nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.$galleries_index_version.index"
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
}
val nodedata = getURLAtRange(url, address.until(address+ max_node_size))
return decodeNode(nodedata)
}
fun getURLAtRange(url: String, range: LongRange) : ByteArray {
val request = Request.Builder()
.url(url)
.header("Range", "bytes=${range.first}-${range.last}")
.build()
return client.newCall(request).execute().body()?.use { it.bytes() } ?: byteArrayOf()
}
@OptIn(ExperimentalUnsignedTypes::class)
data class Node(val keys: List<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>)
@OptIn(ExperimentalUnsignedTypes::class)
fun decodeNode(data: ByteArray) : Node {
val buffer = ByteBuffer
.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
val uData = data.toUByteArray()
val numberOfKeys = buffer.int
val keys = ArrayList<UByteArray>()
for (i in 0.until(numberOfKeys)) {
val keySize = buffer.int
if (keySize == 0 || keySize > 32)
throw Exception("fatal: !keySize || keySize > 32")
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
buffer.position(buffer.position()+keySize)
}
val numberOfDatas = buffer.int
val datas = ArrayList<Pair<Long, Int>>()
for (i in 0.until(numberOfDatas)) {
val offset = buffer.long
val length = buffer.int
datas.add(Pair(offset, length))
}
val numberOfSubNodeAddresses = B +1
val subNodeAddresses = ArrayList<Long>()
for (i in 0.until(numberOfSubNodeAddresses)) {
val subNodeAddress = buffer.long
subNodeAddresses.add(subNodeAddress)
}
return Node(keys, datas, subNodeAddresses)
}
@OptIn(ExperimentalUnsignedTypes::class)
fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
val top = min(dv1.size, dv2.size)
for (i in 0.until(top)) {
if (dv1[i] < dv2[i])
return -1
else if (dv1[i] > dv2[i])
return 1
}
return 0
}
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> {
for (i in node.keys.indices) {
val cmpResult = compareArrayBuffers(key, node.keys[i])
if (cmpResult <= 0)
return Pair(cmpResult==0, i)
}
return Pair(false, node.keys.size)
}
fun isLeaf(node: Node) : Boolean {
for (subnode in node.subNodeAddresses)
if (subnode != 0L)
return false
return true
}
if (node.keys.isEmpty())
return null
val (there, where) = locateKey(key, node)
if (there)
return node.datas[where]
else if (isLeaf(node))
return null
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null
return bSearch(field, key, nextNode)
}

View File

@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -54,13 +55,15 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
val uri = downloadManager.query(query).use { cursor -> val uri = downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).let { cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))?.let {
val uri = Uri.parse(it) val uri = Uri.parse(it)
when (uri.scheme) { when (uri.scheme) {
"file" -> "file" ->
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
) FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!))
else
uri
"content" -> uri "content" -> uri
else -> null else -> null
} }
@@ -73,10 +76,10 @@ class UpdateBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
val pendingIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW).apply { val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")) setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}, 0) }, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0)
val notification = NotificationCompat.Builder(context, "update") val notification = NotificationCompat.Builder(context, "update")
.setSmallIcon(android.R.drawable.stat_sys_download_done) .setSmallIcon(android.R.drawable.stat_sys_download_done)

View File

@@ -23,10 +23,13 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -38,14 +41,10 @@ import okhttp3.ResponseBody
import okio.* import okio.*
import xyz.quaver.pupil.* import xyz.quaver.pupil.*
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.cleanCache import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.ellipsize
import xyz.quaver.pupil.util.normalizeID
import xyz.quaver.pupil.util.requestBuilders
import java.io.IOException import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.log10 import kotlin.math.log10
@@ -75,7 +74,7 @@ class DownloadService : Service() {
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT) getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0)
} }
val action = val action =
NotificationCompat.Action.Builder(0, getText(android.R.string.cancel), NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
@@ -85,7 +84,7 @@ class DownloadService : Service() {
Intent(this, DownloadService::class.java) Intent(this, DownloadService::class.java)
.putExtra(KEY_COMMAND, COMMAND_CANCEL) .putExtra(KEY_COMMAND, COMMAND_CANCEL)
.putExtra(KEY_ID, galleryID), .putExtra(KEY_ID, galleryID),
PendingIntent.FLAG_UPDATE_CURRENT), PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0),
).build() ).build()
notification[galleryID] = NotificationCompat.Builder(this, "download").apply { notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
@@ -168,14 +167,25 @@ class DownloadService : Service() {
private val interceptor: PupilInterceptor = { chain -> private val interceptor: PupilInterceptor = { chain ->
val request = chain.request() val request = chain.request()
var response = chain.proceed(request)
var retry = 5 var response = kotlin.runCatching {
while (!response.isSuccessful && retry > 0) { chain.proceed(request)
response = chain.proceed(request) }.getOrNull()
retry-- var limit = 5
while (response?.isSuccessful != true) {
if (response?.code() == 503) {
Thread.sleep(200)
} else if (--limit < 0)
break
response = kotlin.runCatching {
chain.proceed(request)
}.getOrNull()
} }
checkNotNull(response)
response.newBuilder() response.newBuilder()
.body(response.body()?.let { .body(response.body()?.let {
ProgressResponseBody(request.tag(), it, progressListener) ProgressResponseBody(request.tag(), it, progressListener)
@@ -203,41 +213,40 @@ class DownloadService : Service() {
private val callback = object: Callback { private val callback = object: Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.d("PUPILD", "ONFAILURE ${call.request().tag()}, ${e}")
FirebaseCrashlytics.getInstance().recordException(e)
if (e.message?.contains("cancel", true) == false) { if (e.message?.contains("cancel", true) == false) {
val galleryID = (call.request().tag() as Tag).galleryID val galleryID = (call.request().tag() as Tag).galleryID
// Retry
cancel(galleryID)
download(galleryID)
} }
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}")
val (galleryID, index, startId) = call.request().tag() as Tag val (galleryID, index, startId) = call.request().tag() as Tag
val ext = call.request().url().encodedPath().split('.').last() val ext = call.request().url().encodedPath().split('.').last()
kotlin.runCatching { CoroutineScope(Dispatchers.IO).launch {
val image = response.also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } ?: throw Exception() runCatching {
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt() val image = response.also { if (it.code() != 200) throw IOException( "$galleryID $index ${response.request().url()} CODE ${it.code()}" ) }.body()?.use { it.bytes() } ?: throw Exception("Response null")
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
CoroutineScope(Dispatchers.IO).launch { Cache.getInstance(this@DownloadService, galleryID)
kotlin.runCatching { .putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
Cache.getInstance(this@DownloadService, galleryID).putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
}.onSuccess {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) { progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
if (DownloadManager.getInstance(this@DownloadService) notify(galleryID)
.getDownloadFolder(galleryID) != null)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
startId?.let { stopSelf(it) } if (isCompleted(galleryID)) {
} if (DownloadManager.getInstance(this@DownloadService)
}.onFailure { .getDownloadFolder(galleryID) != null
cancel(galleryID) )
download(galleryID) Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
startId?.let { stopSelf(it) }
} }
}.onFailure {
FirebaseCrashlytics.getInstance().recordException(it)
} }
} }
} }
@@ -294,19 +303,19 @@ class DownloadService : Service() {
} }
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch { fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
cleanCache(this@DownloadService) if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
return@launch
if (progress.containsKey(galleryID)) cleanCache(this@DownloadService)
cancel(galleryID)
val cache = Cache.getInstance(this@DownloadService, galleryID) val cache = Cache.getInstance(this@DownloadService, galleryID)
initNotification(galleryID) initNotification(galleryID)
val reader = cache.getReader() val galleryInfo = cache.getGalleryInfo()
// Gallery doesn't exist // Gallery doesn't exist
if (reader == null) { if (galleryInfo == null) {
delete(galleryID) delete(galleryID)
progress[galleryID] = mutableListOf() progress[galleryID] = mutableListOf()
return@launch return@launch
@@ -314,7 +323,7 @@ class DownloadService : Service() {
histories.add(galleryID) histories.add(galleryID)
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F } progress[galleryID] = MutableList(galleryInfo.files.size) { 0F }
cache.metadata.imageList?.let { cache.metadata.imageList?.let {
it.forEachIndexed { index, image -> it.forEachIndexed { index, image ->
@@ -323,8 +332,7 @@ class DownloadService : Service() {
} }
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService) if (DownloadManager.getInstance(this@DownloadService).getDownloadFolder(galleryID) != null)
.getDownloadFolder(galleryID) != null )
Cache.getInstance(this@DownloadService, galleryID).moveToDownload() Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
@@ -332,7 +340,7 @@ class DownloadService : Service() {
return@launch return@launch
} }
notification[galleryID]?.setContentTitle(reader.galleryInfo.title?.ellipsize(30)) notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30))
notify(galleryID) notify(galleryID)
val queued = mutableSetOf<Int>() val queued = mutableSetOf<Int>()
@@ -346,7 +354,7 @@ class DownloadService : Service() {
} }
} }
reader.requestBuilders.forEachIndexed { index, it -> galleryInfo.getRequestBuilders().forEachIndexed { index, it ->
if (progress[galleryID]?.get(index)?.isInfinite() == false) { if (progress[galleryID]?.get(index)?.isInfinite() == false) {
val request = it.tag(Tag(galleryID, index, startId)).build() val request = it.tag(Tag(galleryID, index, startId)).build()
client.newCall(request).enqueue(callback) client.newCall(request).enqueue(callback)

View File

@@ -0,0 +1,22 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2022 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types
class SendLogException : Exception()
class JavascriptException(message: String?) : Exception(message)

View File

@@ -18,17 +18,22 @@
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.hitomi.Suggestion import xyz.quaver.pupil.hitomi.Suggestion
import xyz.quaver.pupil.util.translations
@Parcelize @Parcelize
data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
@IgnoredOnParcel @IgnoredOnParcel
override val body = s override val body =
if (translations[s] != null)
"${translations[s]} ($s)"
else
s
} }
@Parcelize @Parcelize

View File

@@ -29,10 +29,8 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pin_lock.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.LockActivityBinding
import xyz.quaver.pupil.ui.fragment.PINLockFragment import xyz.quaver.pupil.ui.fragment.PINLockFragment
import xyz.quaver.pupil.ui.fragment.PatternLockFragment import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.util.Lock
@@ -45,6 +43,8 @@ class LockActivity : AppCompatActivity() {
private lateinit var lockManager: LockManager private lateinit var lockManager: LockManager
private var mode: String? = null private var mode: String? = null
private lateinit var binding: LockActivityBinding
private val patternLockFragment = PatternLockFragment().apply { private val patternLockFragment = PatternLockFragment().apply {
var lastPass = "" var lastPass = ""
onPatternDrawn = { onPatternDrawn = {
@@ -57,7 +57,7 @@ class LockActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else } else
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG) binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
} }
"add_lock" -> { "add_lock" -> {
if (lastPass.isEmpty()) { if (lastPass.isEmpty()) {
@@ -69,7 +69,7 @@ class LockActivity : AppCompatActivity() {
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it)) LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
finish() finish()
} else { } else {
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG) binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
lastPass = "" lastPass = ""
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show() Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
@@ -92,15 +92,15 @@ class LockActivity : AppCompatActivity() {
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
finish() finish()
} else { } else {
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply { binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener { setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) { override fun onAnimationEnd(animation: Animation?) {
pin_lock_view.resetPinLockView() binding.pinLockView.resetPinLockView()
pin_lock_view.isEnabled = true binding.pinLockView.isEnabled = true
} }
override fun onAnimationStart(animation: Animation?) { override fun onAnimationStart(animation: Animation?) {
pin_lock_view.isEnabled = false binding.pinLockView.isEnabled = false
} }
override fun onAnimationRepeat(animation: Animation?) { override fun onAnimationRepeat(animation: Animation?) {
@@ -114,22 +114,22 @@ class LockActivity : AppCompatActivity() {
if (lastPass.isEmpty()) { if (lastPass.isEmpty()) {
lastPass = it lastPass = it
pin_lock_view.resetPinLockView() binding.pinLockView.resetPinLockView()
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show() Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
} else { } else {
if (lastPass == it) { if (lastPass == it) {
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it)) LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
finish() finish()
} else { } else {
indicator_dots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply { binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
setAnimationListener(object: Animation.AnimationListener { setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) { override fun onAnimationEnd(animation: Animation?) {
pin_lock_view.resetPinLockView() binding.pinLockView.resetPinLockView()
pin_lock_view.isEnabled = true binding.pinLockView.isEnabled = true
} }
override fun onAnimationStart(animation: Animation?) { override fun onAnimationStart(animation: Animation?) {
pin_lock_view.isEnabled = false binding.pinLockView.isEnabled = false
} }
override fun onAnimationRepeat(animation: Animation?) { override fun onAnimationRepeat(animation: Animation?) {
@@ -173,7 +173,8 @@ class LockActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock) binding = LockActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
lockManager = try { lockManager = try {
LockManager(this) LockManager(this)
@@ -210,7 +211,7 @@ class LockActivity : AppCompatActivity() {
Preferences["lock_fingerprint"] Preferences["lock_fingerprint"]
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS && BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
) { ) {
lock_fingerprint.apply { binding.fingerprintBtn.apply {
isEnabled = true isEnabled = true
setOnClickListener { setOnClickListener {
showBiometricPrompt() showBiometricPrompt()
@@ -219,7 +220,7 @@ class LockActivity : AppCompatActivity() {
showBiometricPrompt() showBiometricPrompt()
} }
lock_pattern.apply { binding.patternBtn.apply {
isEnabled = lockManager.contains(Lock.Type.PATTERN) isEnabled = lockManager.contains(Lock.Type.PATTERN)
setOnClickListener { setOnClickListener {
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -227,7 +228,7 @@ class LockActivity : AppCompatActivity() {
).commit() ).commit()
} }
} }
lock_pin.apply { binding.pinBtn.apply {
isEnabled = lockManager.contains(Lock.Type.PIN) isEnabled = lockManager.contains(Lock.Type.PIN)
setOnClickListener { setOnClickListener {
supportFragmentManager.beginTransaction().replace( supportFragmentManager.beginTransaction().replace(
@@ -235,7 +236,7 @@ class LockActivity : AppCompatActivity() {
).commit() ).commit()
} }
} }
lock_password.isEnabled = false binding.passwordBtn.isEnabled = false
when (lockManager.locks!!.first().type) { when (lockManager.locks!!.first().type) {
Lock.Type.PIN -> { Lock.Type.PIN -> {
@@ -253,20 +254,20 @@ class LockActivity : AppCompatActivity() {
} }
} }
"add_lock" -> { "add_lock" -> {
lock_pattern.isEnabled = false binding.patternBtn.isEnabled = false
lock_pin.isEnabled = false binding.pinBtn.isEnabled = false
lock_fingerprint.isEnabled = false binding.fingerprintBtn.isEnabled = false
lock_password.isEnabled = false binding.passwordBtn.isEnabled = false
when(intent.getStringExtra("type")!!) { when(intent.getStringExtra("type")!!) {
"pattern" -> { "pattern" -> {
lock_pattern.isEnabled = true binding.patternBtn.isEnabled = true
supportFragmentManager.beginTransaction().add( supportFragmentManager.beginTransaction().add(
R.id.lock_content, patternLockFragment R.id.lock_content, patternLockFragment
).commit() ).commit()
} }
"pin" -> { "pin" -> {
lock_pin.isEnabled = true binding.pinBtn.isEnabled = true
supportFragmentManager.beginTransaction().add( supportFragmentManager.beginTransaction().add(
R.id.lock_content, pinLockFragment R.id.lock_content, pinLockFragment
).commit() ).commit()

View File

@@ -21,39 +21,41 @@ package xyz.quaver.pupil.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.text.util.Linkify import android.text.util.Linkify
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.* import android.view.animation.DecelerateInterpolator
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat
import androidx.cardview.widget.CardView
import androidx.core.text.util.LinkifyCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import com.google.android.material.appbar.AppBarLayout import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.MenuView import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.hitomi.doSearch
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.* import xyz.quaver.pupil.*
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.databinding.MainActivityBinding
import xyz.quaver.pupil.hitomi.doSearch
import xyz.quaver.pupil.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.pupil.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.types.* import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment import xyz.quaver.pupil.ui.dialog.DownloadLocationDialogFragment
import xyz.quaver.pupil.ui.dialog.GalleryDialog import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.ui.view.MainView
import xyz.quaver.pupil.ui.view.ProgressCard
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.checkUpdate import xyz.quaver.pupil.util.checkUpdate
@@ -61,8 +63,8 @@ import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -103,18 +105,20 @@ class MainActivity :
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var currentPage = 0 private var currentPage = 0
private lateinit var binding: MainActivityBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.dataString?.let { url -> intent.dataString?.let { url ->
restore(url, restore(url,
onFailure = { onFailure = {
Snackbar.make(this.main_recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = { }, onSuccess = {
Snackbar.make(this.main_recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
} }
) )
} }
@@ -123,15 +127,33 @@ class MainActivity :
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
checkUpdate(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Preferences["download_folder_ignore_warning", false] &&
ContextCompat.getExternalFilesDirs(this, null).filterNotNull().map { Uri.fromFile(it).toString() }
.contains(Preferences["download_folder", ""])
) {
AlertDialog.Builder(this)
.setTitle(R.string.warning)
.setMessage(R.string.unaccessible_download_folder)
.setPositiveButton(android.R.string.ok) { _, _ ->
DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog")
}.setNegativeButton(R.string.ignore) { _, _ ->
Preferences["download_folder_ignore_warning"] = true
}.show()
}
initView() initView()
} }
override fun onResume() {
super.onResume()
checkUpdate(this)
}
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override fun onBackPressed() { override fun onBackPressed() {
when { when {
main_drawer_layout.isDrawerOpen(GravityCompat.START) -> main_drawer_layout.closeDrawer(GravityCompat.START) binding.drawer.isDrawerOpen(GravityCompat.START) -> binding.drawer.closeDrawer(GravityCompat.START)
queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread { queryStack.removeLastOrNull() != null && queryStack.isNotEmpty() -> runOnUiThread {
query = queryStack.last() query = queryStack.last()
@@ -147,7 +169,7 @@ class MainActivity :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
(main_recyclerview?.adapter as? GalleryBlockAdapter)?.updateAll = false (binding.contents.recyclerview.adapter as? GalleryBlockAdapter)?.updateAll = false
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -188,36 +210,36 @@ class MainActivity :
} }
private fun initView() { private fun initView() {
var prevP1 = 0 binding.contents.recyclerview.addOnScrollListener(object: RecyclerView.OnScrollListener() {
main_appbar_layout.addOnOffsetChangedListener( override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
AppBarLayout.OnOffsetChangedListener { _, p1 -> // -height of the search view < translationY < 0
main_searchview.translationY = p1.toFloat() binding.contents.searchview.translationY =
main_recyclerview.scrollBy(0, prevP1 - p1) min(
max(
binding.contents.searchview.translationY - dy,
-binding.contents.searchview.binding.querySection.root.height.toFloat()
), 0F)
with(main_fab) { if (dy > 0)
if (prevP1 > p1) binding.contents.fab.hideMenuButton(true)
hideMenuButton(true) else if (dy < 0)
else if (prevP1 < p1) binding.contents.fab.showMenuButton(true)
showMenuButton(true)
}
prevP1 = p1
} }
) })
Linkify.addLinks(main_noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) }) Linkify.addLinks(binding.contents.noresult, Pattern.compile(getString(R.string.https_text)), null, null, { _, _ -> getString(R.string.https) })
//NavigationView //NavigationView
main_nav_view.setNavigationItemSelectedListener(this) binding.navView.setNavigationItemSelectedListener(this)
with(main_fab_cancel) { with(binding.contents.cancelFab) {
setImageResource(R.drawable.cancel) setImageResource(R.drawable.cancel)
setOnClickListener { setOnClickListener {
DownloadService.cancel(this@MainActivity) DownloadService.cancel(this@MainActivity)
} }
} }
with(main_fab_jump) { with(binding.contents.jumpFab) {
setImageResource(R.drawable.ic_jump) setImageResource(R.drawable.ic_jump)
setOnClickListener { setOnClickListener {
val perPage = Preferences["per_page", "25"].toInt() val perPage = Preferences["per_page", "25"].toInt()
@@ -245,7 +267,7 @@ class MainActivity :
} }
} }
with(main_fab_random) { with(binding.contents.randomFab) {
setImageResource(R.drawable.shuffle_variant) setImageResource(R.drawable.shuffle_variant)
setOnClickListener { setOnClickListener {
runBlocking { runBlocking {
@@ -275,7 +297,7 @@ class MainActivity :
} }
} }
with(main_fab_id) { with(binding.contents.idFab) {
setImageResource(R.drawable.numeric) setImageResource(R.drawable.numeric)
setOnClickListener { setOnClickListener {
val editText = EditText(context).apply { val editText = EditText(context).apply {
@@ -308,6 +330,44 @@ class MainActivity :
} }
} }
with(binding.contents.view) {
setOnPageTurnListener(object: MainView.OnPageTurnListener {
override fun onPrev(page: Int) {
currentPage--
// disable pageturn until the contents are loaded
setCurrentPage(1, false)
ViewCompat.animate(binding.contents.searchview)
.setDuration(100)
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
override fun onNext(page: Int) {
currentPage++
// disable pageturn until the contents are loaded
setCurrentPage(1, false)
ViewCompat.animate(binding.contents.searchview)
.setDuration(100)
.setInterpolator(DecelerateInterpolator())
.translationY(0F)
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
})
}
setupSearchBar() setupSearchBar()
setupRecyclerView() setupRecyclerView()
fetchGalleries(query, sortMode) fetchGalleries(query, sortMode)
@@ -316,7 +376,7 @@ class MainActivity :
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(binding.contents.recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply { adapter = GalleryBlockAdapter(galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
@@ -357,14 +417,12 @@ class MainActivity :
loadBlocks() loadBlocks()
} }
completeFlag.put(galleryID, false)
closeAllItems() closeAllItems()
} }
} }
ItemClickSupport.addTo(this).apply { ItemClickSupport.addTo(this).apply {
onItemClickListener = listener@{ _, position, v -> onItemClickListener = listener@{ _, position, v ->
if (v !is CardView) if (v !is ProgressCard)
return@listener return@listener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
@@ -375,7 +433,7 @@ class MainActivity :
} }
onItemLongClickListener = listener@{ _, position, v -> onItemLongClickListener = listener@{ _, position, v ->
if (v !is CardView) if (v !is ProgressCard)
return@listener false return@listener false
val galleryID = galleries.getOrNull(position) ?: return@listener true val galleryID = galleries.getOrNull(position) ?: return@listener true
@@ -398,207 +456,6 @@ class MainActivity :
true true
} }
} }
var origin = 0f
var target = -1
val perPage = Preferences["per_page", "25"].toInt()
setOnTouchListener { _, event ->
when(event.action) {
MotionEvent.ACTION_UP -> {
origin = 0f
with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(showPrev) {
showPrev = false
val prev = main_recyclerview.layoutManager?.getChildAt(0)
if (prev is LinearLayout) {
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
prev.layoutParams.height = 1
icon.layoutParams.height = 1
icon.rotation = 180f
}
prev?.requestLayout()
notifyItemRemoved(0)
}
if(showNext) {
showNext = false
val next = main_recyclerview.layoutManager?.let {
getChildAt(childCount-1)
}
if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next)
next.layoutParams.height = 1
icon.layoutParams.height = 1
icon.rotation = 0f
}
next?.requestLayout()
notifyItemRemoved(itemCount)
}
}
if (target != -1) {
currentPage = target
runOnUiThread {
cancelFetch()
clearGalleries()
loadBlocks()
}
target = -1
}
}
MotionEvent.ACTION_DOWN -> origin = event.y
MotionEvent.ACTION_MOVE -> {
if (origin == 0f)
origin = event.y
val dist = event.y - origin
when {
!canScrollVertically(-1) -> {
//TOP
//Scrolling UP
if (dist > 0 && currentPage != 0) {
with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(!showPrev) {
showPrev = true
notifyItemInserted(0)
}
}
val prev = main_recyclerview.layoutManager?.getChildAt(0)
if (prev is LinearLayout) {
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
val text = prev.findViewById<TextView>(R.id.text_prev).apply {
text = getString(R.string.main_move, currentPage)
}
if (dist < 360) {
prev.layoutParams.height = (dist/2).roundToInt()
icon.layoutParams.height = (dist/2).roundToInt()
icon.rotation = dist+180
text.layoutParams.width = dist.roundToInt()
target = -1
}
else {
prev.layoutParams.height = 180
icon.layoutParams.height = 180
icon.rotation = 180f
text.layoutParams.width = LinearLayout.LayoutParams.WRAP_CONTENT
target = currentPage-1
}
}
prev?.requestLayout()
return@setOnTouchListener true
} else {
with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(showPrev) {
showPrev = false
val prev = main_recyclerview.layoutManager?.getChildAt(0)
if (prev is LinearLayout) {
val icon = prev.findViewById<ImageView>(R.id.icon_prev)
prev.layoutParams.height = 1
icon.layoutParams.height = 1
icon.rotation = 180f
}
prev?.requestLayout()
notifyItemRemoved(0)
}
}
}
}
!canScrollVertically(1) -> {
//BOTTOM
//Scrolling DOWN
if (dist < 0 && currentPage != ceil(totalItems.toDouble()/perPage).roundToInt()-1) {
with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(!showNext) {
showNext = true
notifyItemInserted(itemCount-1)
}
}
val next = main_recyclerview.layoutManager?.let {
getChildAt(childCount-1)
}
val absDist = abs(dist)
if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next)
val text = next.findViewById<TextView>(R.id.text_next).apply {
text = getString(R.string.main_move, currentPage+2)
}
if (absDist < 360) {
next.layoutParams.height = (absDist/2).roundToInt()
icon.layoutParams.height = (absDist/2).roundToInt()
icon.rotation = -absDist
text.layoutParams.width = absDist.roundToInt()
target = -1
} else {
next.layoutParams.height = 180
icon.layoutParams.height = 180
icon.rotation = 0f
text.layoutParams.width = LinearLayout.LayoutParams.WRAP_CONTENT
target = currentPage+1
}
}
next?.requestLayout()
return@setOnTouchListener true
} else {
with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(showNext) {
showNext = false
val next = main_recyclerview.layoutManager?.let {
getChildAt(childCount-1)
}
if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next)
next.layoutParams.height = 1
icon.layoutParams.height = 1
icon.rotation = 180f
}
next?.requestLayout()
notifyItemRemoved(itemCount)
}
}
}
}
}
}
}
false
}
} }
} }
@@ -619,10 +476,10 @@ class MainActivity :
private var suggestionJob : Job? = null private var suggestionJob : Job? = null
private fun setupSearchBar() { private fun setupSearchBar() {
with(main_searchview as xyz.quaver.pupil.ui.view.FloatingSearchView) { with(binding.contents.searchview) {
onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener { onMenuStatusChangeListener = object: FloatingSearchView.OnMenuStatusChangeListener {
override fun onMenuOpened() { override fun onMenuOpened() {
(this@MainActivity.main_recyclerview.adapter as GalleryBlockAdapter).closeAllItems() (this@MainActivity.binding.contents.recyclerview.adapter as GalleryBlockAdapter).closeAllItems()
} }
override fun onMenuClosed() { override fun onMenuClosed() {
@@ -706,7 +563,7 @@ class MainActivity :
} }
} }
attachNavigationDrawerToMenuButton(main_drawer_layout) attachNavigationDrawerToMenuButton(this@MainActivity.binding.drawer)
} }
} }
@@ -717,7 +574,7 @@ class MainActivity :
val thin = !item.isChecked val thin = !item.isChecked
item.isChecked = thin item.isChecked = thin
main_recyclerview.apply { binding.contents.recyclerview.apply {
(adapter as GalleryBlockAdapter).apply { (adapter as GalleryBlockAdapter).apply {
this.thin = thin this.thin = thin
@@ -758,7 +615,7 @@ class MainActivity :
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
runOnUiThread { runOnUiThread {
main_drawer_layout.closeDrawers() binding.drawer.closeDrawers()
when(item.itemId) { when(item.itemId) {
R.id.main_drawer_home -> { R.id.main_drawer_home -> {
@@ -827,19 +684,17 @@ class MainActivity :
loadingJob?.cancel() loadingJob?.cancel()
} }
private fun clearGalleries() { private fun clearGalleries() = CoroutineScope(Dispatchers.Main).launch {
galleries.clear() galleries.clear()
with(main_recyclerview.adapter as GalleryBlockAdapter?) { with(binding.contents.recyclerview.adapter as GalleryBlockAdapter?) {
this ?: return@with this ?: return@with
this.completeFlag.clear()
this.notifyDataSetChanged() this.notifyDataSetChanged()
} }
main_appbar_layout.setExpanded(true) binding.contents.noresult.visibility = View.INVISIBLE
main_noresult.visibility = View.INVISIBLE binding.contents.progressbar.show()
main_progressbar.show()
} }
private fun fetchGalleries(query: String, sortMode: SortMode) { private fun fetchGalleries(query: String, sortMode: SortMode) {
@@ -854,7 +709,7 @@ class MainActivity :
} }
if (query.isNotEmpty() && mode != Mode.SEARCH) { if (query.isNotEmpty() && mode != Mode.SEARCH) {
Snackbar.make(this@MainActivity.main_recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply { Snackbar.make(binding.contents.recyclerview, R.string.search_all, Snackbar.LENGTH_SHORT).apply {
setAction(android.R.string.ok) { setAction(android.R.string.ok) {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
@@ -932,7 +787,7 @@ class MainActivity :
} }
} }
} }
} }.toList()
} }
} }
@@ -946,18 +801,21 @@ class MainActivity :
throw Exception("No result") throw Exception("No result")
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e !is CancellationException)
if (e.message != "No result")
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
main_noresult.visibility = View.VISIBLE binding.contents.noresult.visibility = View.VISIBLE
main_progressbar.hide() binding.contents.progressbar.hide()
} }
return@launch return@launch
} }
launch(Dispatchers.Main) {
binding.contents.view.setCurrentPage(currentPage + 1, galleryIDs.size > (currentPage+1)*perPage)
}
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks -> galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
@@ -969,10 +827,10 @@ class MainActivity :
}.forEach { }.forEach {
it.await()?.also { it.await()?.also {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
main_progressbar.hide() binding.contents.progressbar.hide()
galleries.add(it) galleries.add(it)
main_recyclerview.adapter!!.notifyItemInserted(galleries.size - 1) binding.contents.recyclerview.adapter!!.notifyItemInserted(galleries.size - 1)
} }
} }
} }

View File

@@ -45,17 +45,14 @@ import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.mlkit.vision.face.Face import com.google.mlkit.vision.face.Face
import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.android.synthetic.main.reader_eye_card.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.Code
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.databinding.NumberpickerDialogBinding
import xyz.quaver.pupil.databinding.ReaderActivityBinding
import xyz.quaver.pupil.favorites import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
@@ -75,7 +72,7 @@ class ReaderActivity : BaseActivity() {
set(value) { set(value) {
field = value field = value
(reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value (binding.recyclerview.adapter as ReaderAdapter).isFullScreen = value
} }
private lateinit var cache: Cache private lateinit var cache: Cache
@@ -118,9 +115,12 @@ class ReaderActivity : BaseActivity() {
private var eyeType: Eye? = null private var eyeType: Eye? = null
private var eyeTime: Long = 0L private var eyeTime: Long = 0L
private lateinit var binding: ReaderActivityBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reader) binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
title = getString(R.string.reader_loading) title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false) supportActionBar?.setDisplayHomeAsUpEnabled(false)
@@ -161,12 +161,10 @@ class ReaderActivity : BaseActivity() {
} }
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.reader, menu) menuInflater.inflate(R.menu.reader, menu)
with(menu?.findItem(R.id.reader_menu_favorite)) { with(menu.findItem(R.id.reader_menu_favorite)) {
this ?: return@with
if (favorites.contains(galleryID)) if (favorites.contains(galleryID))
(icon as Animatable).start() (icon as Animatable).start()
} }
@@ -178,17 +176,19 @@ class ReaderActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) { when(item.itemId) {
R.id.reader_menu_page_indicator -> { R.id.reader_menu_page_indicator -> {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, reader_layout, false) // TODO: Switch to DialogFragment
with(view.dialog_number_picker) { val binding = NumberpickerDialogBinding.inflate(layoutInflater, binding.root, false)
with(binding.numberPicker) {
minValue = 1 minValue = 1
maxValue = cache.metadata.reader?.galleryInfo?.files?.size ?: 0 maxValue = cache.metadata.galleryInfo?.files?.size ?: 0
value = currentPage value = currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
setView(view) setView(binding.root)
}.create() }.create()
view.dialog_ok.setOnClickListener { binding.okButton.setOnClickListener {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.value-1, 0) (this@ReaderActivity.binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(binding.numberPicker.value-1, 0)
dialog.dismiss() dialog.dismiss()
} }
@@ -258,12 +258,12 @@ class ReaderActivity : BaseActivity() {
//currentPage is 1-based //currentPage is 1-based
return when(keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0) (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-2, 0)
true true
} }
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0) (binding.recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
true true
} }
@@ -285,42 +285,35 @@ class ReaderActivity : BaseActivity() {
if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found if (downloader.progress[galleryID]?.isEmpty() == true) { //Gallery not found
update = false update = false
Snackbar Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE) .make(binding.root, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show() .show()
return@launch return@launch
} }
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 binding.downloadProgressbar.max = binding.recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress = binding.downloadProgressbar.progress =
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0 downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0
if (title == getString(R.string.reader_loading)) { if (title == getString(R.string.reader_loading)) {
val reader = cache.metadata.reader val galleryInfo = cache.metadata.galleryInfo
if (reader != null) { if (galleryInfo != null) {
with(reader_recyclerview.adapter as ReaderAdapter) { with(binding.recyclerview.adapter as ReaderAdapter) {
this.reader = reader this.galleryInfo = galleryInfo
notifyDataSetChanged() notifyDataSetChanged()
} }
title = reader.galleryInfo.title title = galleryInfo.title
menu?.findItem(R.id.reader_menu_page_indicator)?.title = menu?.findItem(R.id.reader_menu_page_indicator)?.title =
"$currentPage/${reader.galleryInfo.files.size}" "$currentPage/${galleryInfo.files.size}"
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable( menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.hitomi)
this@ReaderActivity,
when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
}
)
} }
} }
if (downloader.isCompleted(galleryID)) { //Download finished if (downloader.isCompleted(galleryID)) { //Download finished
reader_download_progressbar.visibility = View.GONE binding.downloadProgressbar.visibility = View.GONE
animateDownloadFAB(false) animateDownloadFAB(false)
} }
@@ -329,7 +322,7 @@ class ReaderActivity : BaseActivity() {
} }
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(binding.recyclerview) {
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply { adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = { onItemClickListener = {
if (isScroll) { if (isScroll) {
@@ -339,7 +332,7 @@ class ReaderActivity : BaseActivity() {
scrollMode(false) scrollMode(false)
fullscreen(true) fullscreen(true)
} else { } else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage, 0) //Moves to next page because currentPage is 1-based indexing
} }
} }
} }
@@ -349,9 +342,9 @@ class ReaderActivity : BaseActivity() {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (dy < 0) if (dy < 0)
this@ReaderActivity.reader_fab.showMenuButton(true) binding.fab.showMenuButton(true)
else if (dy > 0) else if (dy > 0)
this@ReaderActivity.reader_fab.hideMenuButton(true) binding.fab.hideMenuButton(true)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager val layoutManager = recyclerView.layoutManager as LinearLayoutManager
@@ -359,12 +352,11 @@ class ReaderActivity : BaseActivity() {
return return
currentPage = layoutManager.findFirstVisibleItemPosition()+1 currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
} }
}) })
} }
with(reader_fab_download) { with(binding.downloadFab) {
animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button animateDownloadFAB(DownloadManager.getInstance(this@ReaderActivity).getDownloadFolder(galleryID) != null) //If download in progress, animate button
setOnClickListener { setOnClickListener {
@@ -381,14 +373,14 @@ class ReaderActivity : BaseActivity() {
} }
} }
with(reader_fab_retry) { with(binding.retryFab) {
setImageResource(R.drawable.refresh) setImageResource(R.drawable.refresh)
setOnClickListener { setOnClickListener {
DownloadService.download(context, galleryID) DownloadService.download(context, galleryID)
} }
} }
with(reader_fab_auto) { with(binding.autoFab) {
setImageResource(R.drawable.eye_white) setImageResource(R.drawable.eye_white)
setOnClickListener { setOnClickListener {
when { when {
@@ -408,13 +400,13 @@ class ReaderActivity : BaseActivity() {
} }
} }
with(reader_fab_fullscreen) { with(binding.fullscreenFab) {
setImageResource(R.drawable.ic_fullscreen) setImageResource(R.drawable.ic_fullscreen)
setOnClickListener { setOnClickListener {
isFullscreen = true isFullscreen = true
fullscreen(isFullscreen) fullscreen(isFullscreen)
this@ReaderActivity.reader_fab.close(true) binding.fab.close(true)
} }
} }
} }
@@ -424,8 +416,8 @@ class ReaderActivity : BaseActivity() {
if (isFullscreen) { if (isFullscreen) {
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
supportActionBar?.hide() supportActionBar?.hide()
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE binding.fab.visibility = View.INVISIBLE
this@ReaderActivity.scroller.let { binding.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height) it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width) it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal) it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb_horizontal)
@@ -434,8 +426,8 @@ class ReaderActivity : BaseActivity() {
} else { } else {
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv() flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
supportActionBar?.show() supportActionBar?.show()
this@ReaderActivity.reader_fab.visibility = View.VISIBLE binding.fab.visibility = View.VISIBLE
this@ReaderActivity.scroller.let { binding.scroller.let {
it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width) it.handleWidth = resources.getDimensionPixelSize(R.dimen.thumb_width)
it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height) it.handleHeight = resources.getDimensionPixelSize(R.dimen.thumb_height)
it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb) it.handleDrawable = ContextCompat.getDrawable(this@ReaderActivity, R.drawable.thumb)
@@ -446,23 +438,27 @@ class ReaderActivity : BaseActivity() {
window.attributes = this window.attributes = this
} }
reader_recyclerview.adapter = reader_recyclerview.adapter // Force to redraw binding.recyclerview.adapter = binding.recyclerview.adapter // Force to redraw
} }
private fun scrollMode(isScroll: Boolean) { private fun scrollMode(isScroll: Boolean) {
if (isScroll) { if (isScroll) {
snapHelper.attachToRecyclerView(null) snapHelper.attachToRecyclerView(null)
reader_recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.layoutManager = LinearLayoutManager(this)
} else { } else {
snapHelper.attachToRecyclerView(reader_recyclerview) snapHelper.attachToRecyclerView(binding.recyclerview)
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false]) binding.recyclerview.layoutManager = object: LinearLayoutManager(this, HORIZONTAL, Preferences["rtl", false]) {
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
extraLayoutSpace.fill(600)
}
}
} }
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0) (binding.recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
} }
private fun animateDownloadFAB(animate: Boolean) { private fun animateDownloadFAB(animate: Boolean) {
with(reader_fab_download) { with(binding.downloadFab) {
if (animate) { if (animate) {
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading) val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
@@ -491,7 +487,7 @@ class ReaderActivity : BaseActivity() {
} }
val cameraCallback: (List<Face>) -> Unit = callback@{ faces -> val cameraCallback: (List<Face>) -> Unit = callback@{ faces ->
eye_card.dot.let { binding.eyeCard.dot.let {
it.visibility = View.VISIBLE it.visibility = View.VISIBLE
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
delay(50) delay(50)
@@ -501,9 +497,9 @@ class ReaderActivity : BaseActivity() {
if (faces.size != 1) if (faces.size != 1)
ContextCompat.getDrawable(this, R.drawable.eye_off).let { ContextCompat.getDrawable(this, R.drawable.eye_off).let {
with(eye_card) { with(binding.eyeCard) {
left_eye.setImageDrawable(it) leftEye.setImageDrawable(it)
right_eye.setImageDrawable(it) rightEye.setImageDrawable(it)
} }
return@callback return@callback
@@ -514,16 +510,16 @@ class ReaderActivity : BaseActivity() {
faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true
) )
with(eye_card) { with(binding.eyeCard) {
left_eye.setImageDrawable( leftEye.setImageDrawable(
ContextCompat.getDrawable( ContextCompat.getDrawable(
context, leftEye.context,
if (left) R.drawable.eye else R.drawable.eye_closed if (left) R.drawable.eye else R.drawable.eye_closed
) )
) )
right_eye.setImageDrawable( rightEye.setImageDrawable(
ContextCompat.getDrawable( ContextCompat.getDrawable(
context, rightEye.context,
if (right) R.drawable.eye else R.drawable.eye_closed if (right) R.drawable.eye else R.drawable.eye_closed
) )
) )
@@ -550,7 +546,7 @@ class ReaderActivity : BaseActivity() {
} }
if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) { if (eyeType != null && System.currentTimeMillis() - eyeTime > 100) {
(this@ReaderActivity.reader_recyclerview.layoutManager as LinearLayoutManager).let { (binding.recyclerview.layoutManager as LinearLayoutManager).let {
it.scrollToPositionWithOffset(when(eyeType!!) { it.scrollToPositionWithOffset(when(eyeType!!) {
Eye.RIGHT -> { Eye.RIGHT -> {
if (it.reverseLayout) currentPage - 2 else currentPage if (it.reverseLayout) currentPage - 2 else currentPage
@@ -566,11 +562,11 @@ class ReaderActivity : BaseActivity() {
} }
private fun toggleCamera() { private fun toggleCamera() {
val eyes = this@ReaderActivity.eye_card val eyes = binding.eyeCard.root
when (camera) { when (camera) {
null -> { null -> {
reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel) binding.autoFab.labelText = getString(R.string.reader_fab_auto_cancel)
reader_fab_auto.setImageResource(R.drawable.eye_off_white) binding.autoFab.setImageResource(R.drawable.eye_off_white)
eyes.apply { eyes.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
TranslateAnimation(0F, 0F, -100F, 0F).apply { TranslateAnimation(0F, 0F, -100F, 0F).apply {
@@ -583,8 +579,8 @@ class ReaderActivity : BaseActivity() {
cameraEnabled = true cameraEnabled = true
} }
else -> { else -> {
reader_fab_auto.labelText = getString(R.string.reader_fab_auto) binding.autoFab.labelText = getString(R.string.reader_fab_auto)
reader_fab_auto.setImageResource(R.drawable.eye_white) binding.autoFab.setImageResource(R.drawable.eye_white)
eyes.apply { eyes.apply {
TranslateAnimation(0F, 0F, 0F, -100F).apply { TranslateAnimation(0F, 0F, 0F, -100F).apply {
duration = 500 duration = 500

View File

@@ -18,30 +18,30 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_default_query.* import androidx.fragment.app.DialogFragment
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
class DefaultQueryDialog(context : Context) : AlertDialog(context) { class DefaultQueryDialog : DialogFragment() {
private val languages = context.resources.getStringArray(R.array.languages).map { private val languages: Map<String, String> by lazy {
it.split("|").let { split -> requireContext().resources.getStringArray(R.array.languages).map {
Pair(split[0], split[1]) it.split("|").let { split ->
} Pair(split[0], split[1])
}.toMap() }
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k } }.toMap()
}
private val reverseLanguages: Map<String, String> by lazy {
languages.entries.associate { (k, v) -> v to k }
}
private val excludeBL = "-male:yaoi" private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro") private val excludeGuro = listOf("-female:guro", "-male:guro")
@@ -49,46 +49,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@SuppressLint("InflateParams") private var _binding: DefaultQueryDialogBinding? = null
override fun onCreate(savedInstanceState: Bundle?) { private val binding get() = _binding!!
setTitle(R.string.default_query_dialog_title)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
with(default_query_dialog_language_selector) { private fun initView() {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
if (default_query_dialog_loli_checkbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
super.onCreate(savedInstanceState)
}
@SuppressLint("InflateParams")
private fun build() : View {
val tags = Tags.parse( val tags = Tags.parse(
Preferences["default_query"] Preferences["default_query"]
) )
val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null) with(binding.languageSelector) {
with(view.default_query_dialog_language_selector) {
adapter = adapter =
ArrayAdapter( ArrayAdapter(
context, context,
@@ -111,13 +80,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(view.default_query_dialog_BL_checkbox) { with(binding.BLCheckbox) {
isChecked = tags.contains(excludeBL) isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL)) if (tags.contains(excludeBL))
tags.remove(excludeBL) tags.remove(excludeBL)
} }
with(view.default_query_dialog_guro_checkbox) { with(binding.guroCheckbox) {
isChecked = excludeGuro.all { tags.contains(it) } isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) }) if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach { excludeGuro.forEach {
@@ -125,7 +94,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(view.default_query_dialog_loli_checkbox) { with(binding.loliCheckbox) {
isChecked = excludeLoli.all { tags.contains(it) } isChecked = excludeLoli.all { tags.contains(it) }
if (excludeLoli.all { tags.contains(it) }) if (excludeLoli.all { tags.contains(it) })
excludeLoli.forEach { excludeLoli.forEach {
@@ -133,7 +102,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
} }
with(view.default_query_dialog_edittext) { with(binding.edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher { addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged( override fun beforeTextChanged(
@@ -158,8 +127,45 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
} }
}) })
} }
}
return view override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DefaultQueryDialogBinding.inflate(layoutInflater)
initView()
return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.default_query_dialog_title)
setView(binding.root)
setPositiveButton(android.R.string.ok) { _, _ ->
val newTags = Tags.parse(binding.edittext.text.toString())
with(binding.languageSelector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (binding.BLCheckbox.isChecked)
newTags.add(excludeBL)
if (binding.guroCheckbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
if (binding.loliCheckbox.isChecked)
excludeLoli.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
}.create()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
} }
} }

View File

@@ -18,17 +18,17 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadFolderNameDialogBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.formatDownloadFolder import xyz.quaver.pupil.util.formatDownloadFolder
@@ -37,38 +37,49 @@ import xyz.quaver.pupil.util.formatMap
class DownloadFolderNameDialogFragment : DialogFragment() { class DownloadFolderNameDialogFragment : DialogFragment() {
@SuppressLint("InflateParams") private var _binding: DownloadFolderNameDialogBinding? = null
private fun build(): View { private val binding get() = _binding!!
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
val galleryBlock = runBlocking {
Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
}
return layoutInflater.inflate(R.layout.dialog_download_folder_name, null).apply { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "") _binding = DownloadFolderNameDialogBinding.inflate(layoutInflater)
edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
edittext.addTextChangedListener {
message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
}
ok_button.setOnClickListener {
val newValue = edittext.text.toString()
if ((newValue as? String)?.contains("/") != false) { initView()
Snackbar.make(this, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
}
Preferences["download_folder_name"] = edittext.text.toString() return Dialog(requireContext()).apply {
setContentView(binding.root)
dismiss() window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT
}
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = override fun onDestroy() {
Dialog(requireContext()).apply { super.onDestroy()
setContentView(build()) _binding = null
window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT }
}
private fun initView() {
val galleryID = Cache.instances.let { if (it.size == 0) 1199708 else it.keys.elementAt((0 until it.size).random()) }
CoroutineScope(Dispatchers.IO).launch {
val galleryBlock = Cache.getInstance(requireContext(), galleryID).getGalleryBlock()
binding.message.text = getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolder() ?: "")
binding.edittext.addTextChangedListener {
binding.message.text = requireContext().getString(R.string.settings_download_folder_name_message, formatMap.keys.toString(), galleryBlock?.formatDownloadFolderTest(it.toString()) ?: "")
}
}
binding.edittext.setText(Preferences["download_folder_name", "[-id-] -title-"])
binding.okButton.setOnClickListener {
val newValue = binding.edittext.text.toString()
if ((newValue as? String)?.contains("/") != false) {
Snackbar.make(binding.root, R.string.settings_invalid_download_folder_name, Snackbar.LENGTH_SHORT).show()
return@setOnClickListener
}
Preferences["download_folder_name"] = binding.edittext.text.toString()
dismiss()
}
}
} }

View File

@@ -18,52 +18,49 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_download_folder_name.view.*
import kotlinx.android.synthetic.main.item_download_folder.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.toFile import xyz.quaver.io.util.toFile
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DownloadLocationDialogBinding
import xyz.quaver.pupil.databinding.DownloadLocationItemBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.migrate
import java.io.File import java.io.File
class DownloadLocationDialogFragment : DialogFragment() { class DownloadLocationDialogFragment : DialogFragment() {
private val entries = mutableMapOf<File?, View>()
private var _binding: DownloadLocationDialogBinding? = null
private val binding get() = _binding!!
private val entries = mutableMapOf<File?, DownloadLocationItemBinding>()
private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val requestDownloadFolderLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
val activity = activity ?: return@registerForActivityResult
val context = context ?: return@registerForActivityResult val context = context ?: return@registerForActivityResult
val dialog = dialog ?: return@registerForActivityResult val dialog = dialog ?: return@registerForActivityResult
it.data?.data?.also { uri -> it.data?.data?.also { uri ->
val takeFlags: Int = val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.intent.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
context.contentResolver.takePersistableUriPermission(uri, takeFlags) context.contentResolver.takePersistableUriPermission(uri, takeFlags)
if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) { if (kotlin.runCatching { FileX(context, uri).canWrite() }.getOrDefault(false)) {
entries[null]?.location_available?.text = uri.toFile(context)?.canonicalPath entries[null]?.locationAvailable?.text = uri.toFile(context)?.canonicalPath
Preferences["download_folder"] = uri.toString() Preferences["download_folder"] = uri.toString()
} else { } else {
Snackbar.make( Snackbar.make(
@@ -75,9 +72,18 @@ class DownloadLocationDialogFragment : DialogFragment() {
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder } val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
} }
} }
} else {
val downloadFolder = DownloadManager.getInstance(context ?: return@registerForActivityResult).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
if (key == null)
entries[key]!!.locationAvailable.text = downloadFolder
else {
entries[null]!!.button.isChecked = false
entries[key]!!.button.isChecked = true
}
} }
} }
@@ -98,51 +104,46 @@ class DownloadLocationDialogFragment : DialogFragment() {
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder } val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
} }
else { else {
entries[null]?.location_available?.text = directory entries[null]?.locationAvailable?.text = directory
Preferences["download_folder"] = File(directory).toURI().toString() Preferences["download_folder"] = File(directory).toURI().toString()
} }
} }
} }
@SuppressLint("InflateParams") private fun initView() {
private fun build() : View? { val externalFilesDirs = ContextCompat.getExternalFilesDirs(requireContext(), null)
val context = context ?: return null
val view = layoutInflater.inflate(R.layout.dialog_download_folder, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
externalFilesDirs.forEachIndexed { index, dir -> externalFilesDirs.forEachIndexed { index, dir ->
dir ?: return@forEachIndexed dir ?: return@forEachIndexed
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply { DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
location_type.text = context.getString(when (index) { locationType.text = requireContext().getString(when (index) {
0 -> R.string.settings_download_folder_internal 0 -> R.string.settings_download_folder_internal
else -> R.string.settings_download_folder_removable else -> R.string.settings_download_folder_removable
}) })
location_available.text = context.getString( locationAvailable.text = requireContext().getString(
R.string.settings_download_folder_available, R.string.settings_download_folder_available,
byteToString(dir.freeSpace) byteToString(dir.freeSpace)
) )
setOnClickListener { root.setOnClickListener {
entries.values.forEach { entries.values.forEach { entry ->
it.button.isChecked = false entry.button.isChecked = false
} }
button.performClick() button.performClick()
Preferences["download_folder"] = dir.toUri().toString() Preferences["download_folder"] = dir.toUri().toString()
} }
entries[dir] = this entries[dir] = this
}) }
} }
view.addView(layoutInflater.inflate(R.layout.item_download_folder, view, false).apply { DownloadLocationItemBinding.inflate(layoutInflater, binding.root, true).apply {
location_type.text = context.getString(R.string.settings_download_folder_custom) locationType.text = requireContext().getString(R.string.settings_download_folder_custom)
setOnClickListener { root.setOnClickListener {
entries.values.forEach { entries.values.forEach { entry ->
it.button.isChecked = false entry.button.isChecked = false
} }
button.performClick() button.performClick()
@@ -166,31 +167,33 @@ class DownloadLocationDialogFragment : DialogFragment() {
} }
} }
entries[null] = this entries[null] = this
}) }
val downloadFolder = DownloadManager.getInstance(context).downloadFolder.canonicalPath val downloadFolder = DownloadManager.getInstance(requireContext()).downloadFolder.canonicalPath
val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder } val key = entries.keys.firstOrNull { it?.canonicalPath == downloadFolder }
entries[key]!!.button.isChecked = true entries[key]!!.button.isChecked = true
if (key == null) entries[key]!!.location_available.text = downloadFolder if (key == null) entries[key]!!.locationAvailable.text = downloadFolder
return view
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext()) _binding = DownloadLocationDialogBinding.inflate(layoutInflater)
builder initView()
.setTitle(R.string.settings_download_folder)
.setView(build()) return AlertDialog.Builder(requireContext()).apply {
.setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ -> setTitle(R.string.settings_download_folder)
setView(binding.root)
setPositiveButton(requireContext().getText(android.R.string.ok)) { _, _ ->
if (Preferences["download_folder", ""].isEmpty()) if (Preferences["download_folder", ""].isEmpty())
Preferences["download_folder"] = context?.getExternalFilesDir(null)?.toUri()?.toString() ?: "" Preferences["download_folder"] = context.getExternalFilesDir(null)?.toUri()?.toString() ?: ""
DownloadManager.getInstance(requireContext()).migrate()
} }
isCancelable = false isCancelable = false
}.create()
}
return builder.create() override fun onDestroy() {
super.onDestroy()
_binding = null
} }
} }

View File

@@ -22,7 +22,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout.LayoutParams import android.widget.LinearLayout.LayoutParams
@@ -32,21 +31,17 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.dialog_gallery_details.view.*
import kotlinx.android.synthetic.main.dialog_gallery_dotindicator.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import xyz.quaver.hitomi.Gallery import xyz.quaver.pupil.hitomi.Gallery
import xyz.quaver.hitomi.getGallery import xyz.quaver.pupil.hitomi.getGallery
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailPageAdapter import xyz.quaver.pupil.adapters.ThumbnailPageAdapter
import xyz.quaver.pupil.databinding.*
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.ui.view.TagChip import xyz.quaver.pupil.ui.view.TagChip
@@ -60,9 +55,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>() val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
private lateinit var binding: GalleryDialogBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_gallery) binding = GalleryDialogBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.attributes.apply { window?.attributes.apply {
this ?: return@apply this ?: return@apply
@@ -71,7 +69,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
height = LayoutParams.MATCH_PARENT height = LayoutParams.MATCH_PARENT
} }
with(gallery_fab) { with(binding.fab) {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right)) setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
setOnClickListener { setOnClickListener {
context.startActivity(Intent(context, ReaderActivity::class.java).apply { context.startActivity(Intent(context, ReaderActivity::class.java).apply {
@@ -84,12 +82,12 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
try { try {
val gallery = getGallery(galleryID) val gallery = getGallery(galleryID)
gallery_cover.post { launch (Dispatchers.Main) {
gallery_progressbar.visibility = View.GONE binding.progressbar.visibility = View.GONE
gallery_title.text = gallery.title binding.title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() } binding.artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
with(gallery_type) { with(binding.type) {
text = gallery.type.wordCapitalize() text = gallery.type.wordCapitalize()
setOnClickListener { setOnClickListener {
gallery.type.let { gallery.type.let {
@@ -106,14 +104,14 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
} }
gallery_cover.showImage(Uri.parse(gallery.cover)) binding.cover.showImage(Uri.parse(gallery.cover))
addDetails(gallery) addDetails(gallery)
addThumbnails(gallery) addThumbnails(gallery)
addRelated(gallery) addRelated(gallery)
} }
} catch (e: Exception) { } catch (e: Exception) {
Snackbar.make(gallery_layout, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply { Snackbar.make(binding.root, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).apply {
if (Locale.getDefault().language == "ko") if (Locale.getDefault().language == "ko")
setAction(context.getText(R.string.https_text)) { setAction(context.getText(R.string.https_text)) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https)))) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.https))))
@@ -124,10 +122,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
private fun addDetails(gallery: Gallery) { private fun addDetails(gallery: Gallery) {
val inflater = LayoutInflater.from(context) GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
type.setText(R.string.gallery_details)
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details)
listOf( listOf(
R.string.gallery_artists, R.string.gallery_artists,
@@ -164,13 +160,13 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
) )
).filter { ).filter {
(_, content) -> content.isNotEmpty() (_, content) -> content.isNotEmpty()
}.forEach { (title, content) -> }.forEach { (title, content) ->
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply { GalleryDialogTagsBinding.inflate(layoutInflater, contents, true).apply {
gallery_details_type.setText(title) type.setText(title)
content.forEach { tag -> content.forEach { tag ->
gallery_details_tags.addView( tags.addView(
TagChip(context, tag).apply { TagChip(context, tag).apply {
setOnClickListener { setOnClickListener {
onChipClickedHandler.forEach { handler -> onChipClickedHandler.forEach { handler ->
@@ -180,42 +176,34 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
) )
} }
}.let {
gallery_details_contents.addView(it)
} }
} }
}.let {
gallery_contents.addView(it)
} }
} }
private fun addThumbnails(gallery: Gallery) { private fun addThumbnails(gallery: Gallery) {
val inflater = LayoutInflater.from(context) GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
type.setText(R.string.gallery_thumbnails)
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails)
val pager = ViewPager2(context).apply { val pager = ViewPager2(context).apply {
adapter = ThumbnailPageAdapter(gallery.thumbnails) adapter = ThumbnailPageAdapter(gallery.thumbnails)
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
} }
gallery_details_contents.addView( contents.addView(
pager, pager,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
) )
LayoutInflater.from(context).inflate(R.layout.dialog_gallery_dotindicator, gallery_details_contents) // TODO: Change to direct allocation
GalleryDialogDotindicatorBinding.inflate(layoutInflater, contents, true).apply {
gallery_dotindicator.setViewPager2(pager) dotindicator.setViewPager2(pager)
}.let { }
gallery_contents.addView(it)
} }
} }
private fun addRelated(gallery: Gallery) { private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context) val galleries = mutableListOf<Int>()
val galleries = ArrayList<Int>()
val adapter = GalleryBlockAdapter(galleries).apply { val adapter = GalleryBlockAdapter(galleries).apply {
onChipClickedHandler.add { tag -> onChipClickedHandler.add { tag ->
@@ -225,10 +213,10 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
} }
} }
inflater.inflate(R.layout.dialog_gallery_details, gallery_contents, false).apply { GalleryDialogDetailsBinding.inflate(layoutInflater, binding.contents, true).apply {
gallery_details.setText(R.string.gallery_related) type.setText(R.string.gallery_related)
RecyclerView(context).apply { contents.addView(RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
this.adapter = adapter this.adapter = adapter
@@ -248,22 +236,18 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
true true
} }
} }
}.let { })
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gallery.related.forEach { galleryID -> gallery.related.forEach { galleryID ->
Cache.getInstance(context, galleryID).getGalleryBlock()?.let { Cache.getInstance(context, galleryID).getGalleryBlock()?.let {
galleries.add(galleryID) galleries.add(galleryID)
}
} }
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
}
} }
} }
} }

View File

@@ -1,91 +0,0 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.MirrorAdapter
import xyz.quaver.pupil.util.Preferences
class MirrorDialog(context: Context) : AlertDialog(context) {
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
var onMoveItem : ((Int, Int) -> (Unit))? = null
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
setTitle(R.string.settings_mirror_title)
setView(build())
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
super.onCreate(savedInstanceState)
}
private fun build() : View {
return RecyclerView(context).apply recyclerview@{
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(context)
adapter = MirrorAdapter(context).apply adapter@{
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
onMoveItem = this@adapter.onItemMove
}).apply {
attachToRecyclerView(this@recyclerview)
}
onStartDrag = {
itemTouchHelper.startDrag(it)
}
onItemMoved = {
Preferences["mirrors"] = it.joinToString(">")
}
}
}
}
}

View File

@@ -18,58 +18,60 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_proxy.view.* import androidx.fragment.app.DialogFragment
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.databinding.ProxyDialogBinding
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.ProxyInfo import xyz.quaver.pupil.util.ProxyInfo
import xyz.quaver.pupil.util.getProxyInfo import xyz.quaver.pupil.util.getProxyInfo
import xyz.quaver.pupil.util.proxyInfo import xyz.quaver.pupil.util.proxyInfo
import java.net.Proxy import java.net.Proxy
class ProxyDialog(context: Context) : AlertDialog(context) { class ProxyDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { private var _binding: ProxyDialogBinding? = null
setView(build()) private val binding get() = _binding!!
super.onCreate(savedInstanceState) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = ProxyDialogBinding.inflate(layoutInflater)
initView()
return AlertDialog.Builder(requireContext()).apply {
setView(binding.root)
}.create()
} }
@SuppressLint("InflateParams") private fun initView() {
private fun build() : View {
val proxyInfo = getProxyInfo() val proxyInfo = getProxyInfo()
val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null)
val enabler = { enable: Boolean -> val enabler = { enable: Boolean ->
view?.proxy_addr?.isEnabled = enable binding.addr.isEnabled = enable
view?.proxy_port?.isEnabled = enable binding.port.isEnabled = enable
view?.proxy_username?.isEnabled = enable binding.username.isEnabled = enable
view?.proxy_password?.isEnabled = enable binding.password.isEnabled = enable
if (!enable) { if (!enable) {
view?.proxy_addr?.text = null binding.addr.text = null
view?.proxy_port?.text = null binding.port.text = null
view?.proxy_username?.text = null binding.username.text = null
view?.proxy_password?.text = null binding.password.text = null
} }
} }
with(view.proxy_type_selector) { with(binding.typeSelector) {
adapter = ArrayAdapter( adapter = ArrayAdapter(
context, context,
android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_dropdown_item,
@@ -87,29 +89,29 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
} }
} }
view.proxy_addr.setText(proxyInfo.host) binding.addr.setText(proxyInfo.host)
view.proxy_port.setText(proxyInfo.port?.toString()) binding.port.setText(proxyInfo.port?.toString())
view.proxy_username.setText(proxyInfo.username) binding.username.setText(proxyInfo.username)
view.proxy_password.setText(proxyInfo.password) binding.password.setText(proxyInfo.password)
enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT) enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT)
view.proxy_cancel.setOnClickListener { binding.cancelButton.setOnClickListener {
dismiss() dismiss()
} }
view.proxy_ok.setOnClickListener { binding.okButton.setOnClickListener {
val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition] val type = Proxy.Type.values()[binding.typeSelector.selectedItemPosition]
val addr = view.proxy_addr.text?.toString() val addr = binding.addr.text?.toString()
val port = view.proxy_port.text?.toString()?.toIntOrNull() val port = binding.port.text?.toString()?.toIntOrNull()
val username = view.proxy_username.text?.toString() val username = binding.username.text?.toString()
val password = view.proxy_password.text?.toString() val password = binding.password.text?.toString()
if (type != Proxy.Type.DIRECT) { if (type != Proxy.Type.DIRECT) {
if (addr == null || addr.isEmpty()) if (addr == null || addr.isEmpty())
view.proxy_addr.error = context.getText(R.string.proxy_dialog_error) binding.addr.error = requireContext().getText(R.string.proxy_dialog_error)
if (port == null) if (port == null)
view.proxy_port.error = context.getText(R.string.proxy_dialog_error) binding.port.error = requireContext().getText(R.string.proxy_dialog_error)
if (addr == null || addr.isEmpty() || port == null) if (addr == null || addr.isEmpty() || port == null)
return@setOnClickListener return@setOnClickListener
@@ -126,8 +128,6 @@ class ProxyDialog(context: Context) : AlertDialog(context) {
dismiss() dismiss()
} }
return view
} }
} }

View File

@@ -19,21 +19,30 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.content.Intent import android.content.Intent
import android.content.res.Resources
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import okhttp3.* import okhttp3.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.util.restore import xyz.quaver.pupil.util.restore
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.math.roundToInt
class ManageFavoritesFragment : PreferenceFragmentCompat() { class ManageFavoritesFragment : PreferenceFragmentCompat() {
@@ -47,21 +56,62 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
val context = context ?: return val context = context ?: return
findPreference<Preference>("backup")?.setOnPreferenceClickListener { findPreference<Preference>("backup")?.setOnPreferenceClickListener {
val iconSize = (24 * Resources.getSystem().displayMetrics.density).roundToInt()
val strokeWidth = (3 * Resources.getSystem().displayMetrics.density)
val icon = object: CircularProgressDrawable(context) {
override fun getIntrinsicHeight(): Int {
return iconSize
}
override fun getIntrinsicWidth(): Int {
return iconSize
}
}
icon.strokeWidth = strokeWidth
icon.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
DrawableCompat.setTint(icon, ContextCompat.getColor(context, R.color.colorAccent))
icon.start()
it.icon = icon
val favorites = runCatching {
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites.json").readText())
}.getOrNull()
val favoriteTags = kotlin.runCatching {
Json.parseToJsonElement(File(ContextCompat.getDataDir(context), "favorites_tags.json").readText())
}.getOrNull()
val request = Request.Builder() val request = Request.Builder()
.url(context.getString(R.string.backup_url)) .url(context.getString(R.string.backup_url))
.post( .post(
FormBody.Builder() FormBody.Builder()
.add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText()) .add("f:1", buildJsonObject {
favorites?.let {
put("favorites", it)
}
favoriteTags?.let {
put("favorite_tags", it)
}
}.toString())
.build() .build()
).build() ).build()
client.newCall(request).enqueue(object: Callback { client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
val view = view ?: return val view = view ?: return
MainScope().launch {
it.icon = null
}
Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show()
} }
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
MainScope().launch {
it.icon = null
}
if (response.code() != 200) { if (response.code() != 200) {
response.close() response.close()
return return
@@ -93,7 +143,7 @@ class ManageFavoritesFragment : PreferenceFragmentCompat() {
Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show() Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show()
}, onSuccess = onSuccess@{ }, onSuccess = onSuccess@{
val view = view ?: return@onSuccess val view = view ?: return@onSuccess
Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() Snackbar.make(view, context.getString(R.string.settings_restore_success, it), Snackbar.LENGTH_LONG).show()
}) })
}.setNegativeButton(android.R.string.cancel) { _, _ -> }.setNegativeButton(android.R.string.cancel) { _, _ ->
// Do Nothing // Do Nothing

View File

@@ -18,22 +18,38 @@
package xyz.quaver.pupil.ui.fragment package xyz.quaver.pupil.ui.fragment
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.SAFileX
import xyz.quaver.io.util.deleteRecursively import xyz.quaver.io.util.deleteRecursively
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.json
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File import java.io.File
import kotlin.math.roundToInt
class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener {
@@ -45,12 +61,10 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
initPreferences() initPreferences()
} }
override fun onPreferenceClick(preference: Preference?): Boolean { override fun onPreferenceClick(preference: Preference): Boolean {
val context = context ?: return false val context = context ?: return false
with(preference) { with(preference) {
this ?: return false
when (key) { when (key) {
"delete_cache" -> { "delete_cache" -> {
val dir = File(context.cacheDir, "imageCache") val dir = File(context.cacheDir, "imageCache")
@@ -80,6 +94,48 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
setNegativeButton(android.R.string.cancel) { _, _ -> } setNegativeButton(android.R.string.cancel) { _, _ -> }
}.show() }.show()
} }
"recover_downloads" -> {
val density = context.resources.displayMetrics.density
this.icon = object: CircularProgressDrawable(context) {
override fun getIntrinsicHeight() = (24*density).roundToInt()
override fun getIntrinsicWidth() = (24*density).roundToInt()
}.apply {
setStyle(CircularProgressDrawable.DEFAULT)
colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.colorAccent), PorterDuff.Mode.SRC_IN)
start()
}
val downloadManager = DownloadManager.getInstance(context)
val downloadFolderMap = downloadManager.downloadFolderMap
downloadFolderMap.clear()
downloadManager.downloadFolder.listFiles { file -> file.isDirectory }?.forEach { folder ->
val metadataFile = FileX(context, folder, ".metadata")
if (!metadataFile.exists()) return@forEach
val metadata = metadataFile.readText()?.let {
runCatching {
json.decodeFromString<Metadata>(it)
}.getOrNull()
} ?: return@forEach
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach
downloadFolderMap[galleryID] = folder.name
}
downloadManager.downloadFolderMap.putAll(downloadFolderMap)
val downloads = FileX(context, downloadManager.downloadFolder, ".download")
if (!downloads.exists()) downloads.createNewFile()
downloads.writeText(Json.encodeToString(downloadFolderMap))
this.icon = null
Toast.makeText(context, android.R.string.ok, Toast.LENGTH_SHORT).show()
}
"delete_downloads" -> { "delete_downloads" -> {
val dir = DownloadManager.getInstance(context).downloadFolder val dir = DownloadManager.getInstance(context).downloadFolder
@@ -94,7 +150,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
} }
if (dir.exists()) if (dir.exists())
dir.listFiles()?.forEach { (it as? FileX)?.deleteRecursively() } dir.listFiles()?.forEach {
when (it) {
is FileX -> it.deleteRecursively()
else -> it.deleteRecursively()
}
}
job = launch { job = launch {
var size = 0L var size = 0L
@@ -186,6 +247,12 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
onPreferenceClickListener = this@ManageStorageFragment onPreferenceClickListener = this@ManageStorageFragment
} }
with(findPreference<Preference>("recover_downloads")) {
this ?: return@with
onPreferenceClickListener = this@ManageStorageFragment
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -24,30 +24,34 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.andrognito.pinlockview.PinLockListener import com.andrognito.pinlockview.PinLockListener
import kotlinx.android.synthetic.main.fragment_pin_lock.view.* import xyz.quaver.pupil.databinding.PinLockFragmentBinding
import xyz.quaver.pupil.R
class PINLockFragment : Fragment(), PinLockListener { class PINLockFragment : Fragment() {
private var _binding: PinLockFragmentBinding? = null
val binding get() = _binding!!
var onPINEntered: ((String) -> Unit)? = null var onPINEntered: ((String) -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_pin_lock, container, false).apply { _binding = PinLockFragmentBinding.inflate(inflater, container, false)
pin_lock_view.attachIndicatorDots(indicator_dots)
pin_lock_view.setPinLockListener(this@PINLockFragment) binding.pinLockView.attachIndicatorDots(binding.indicatorDots)
} binding.pinLockView.setPinLockListener(object: PinLockListener {
override fun onComplete(p0: String?) {
onPINEntered?.invoke(p0 ?: "")
}
override fun onEmpty() {}
override fun onPinChange(p0: Int, p1: String?) {}
})
return binding.root
} }
override fun onComplete(pin: String?) { override fun onDestroy() {
onPINEntered?.invoke(pin!!) super.onDestroy()
} _binding = null
override fun onEmpty() {
}
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
} }
} }

View File

@@ -26,38 +26,36 @@ import androidx.fragment.app.Fragment
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
import com.andrognito.patternlockview.listener.PatternLockViewListener import com.andrognito.patternlockview.listener.PatternLockViewListener
import com.andrognito.patternlockview.utils.PatternLockUtils import com.andrognito.patternlockview.utils.PatternLockUtils
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import xyz.quaver.pupil.databinding.PatternLockFragmentBinding
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
import xyz.quaver.pupil.R
class PatternLockFragment : Fragment(), PatternLockViewListener { class PatternLockFragment : Fragment() {
private var _binding: PatternLockFragmentBinding? = null
val binding get() = _binding!!
var onPatternDrawn: ((String) -> Unit)? = null var onPatternDrawn: ((String) -> Unit)? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply { _binding = PatternLockFragmentBinding.inflate(inflater, container, false)
lock_pattern_view.addPatternLockListener(this@PatternLockFragment) binding.patternLockView.addPatternLockListener(object: PatternLockViewListener {
} override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
val password = PatternLockUtils.patternToMD5(binding.patternLockView, pattern)
onPatternDrawn?.invoke(password)
}
override fun onCleared() {}
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {}
override fun onStarted() {}
})
return binding.root
} }
override fun onCleared() { override fun onDestroy() {
super.onDestroy()
} _binding = null
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
val password = PatternLockUtils.patternToMD5(lock_pattern_view, pattern)
onPatternDrawn?.invoke(password)
}
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {
}
override fun onStarted() {
} }
} }

View File

@@ -24,19 +24,27 @@ import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.Preference import androidx.preference.*
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Dispatcher
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.clientBuilder
import xyz.quaver.pupil.clientHolder
import xyz.quaver.pupil.types.SendLogException
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.* import xyz.quaver.pupil.ui.dialog.*
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
import java.util.*
import java.util.concurrent.Executors
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@@ -72,10 +80,8 @@ class SettingsFragment :
} }
} }
override fun onPreferenceClick(preference: Preference?): Boolean { override fun onPreferenceClick(preference: Preference): Boolean {
with (preference) { with (preference) {
this ?: return false
when (key) { when (key) {
"app_version" -> { "app_version" -> {
checkUpdate(activity as SettingsActivity, true) checkUpdate(activity as SettingsActivity, true)
@@ -84,12 +90,12 @@ class SettingsFragment :
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
} }
"default_query" -> { "default_query" -> {
DefaultQueryDialog(requireContext()).apply { DefaultQueryDialog().apply {
onPositiveButtonClickListener = { newTags -> onPositiveButtonClickListener = { newTags ->
Preferences["default_query"] = newTags.toString() Preferences["default_query"] = newTags.toString()
summary = newTags.toString() summary = newTags.toString()
} }
}.show() }.show(parentFragmentManager, "Default Query Dialog")
} }
"app_lock" -> { "app_lock" -> {
val intent = Intent(requireContext(), LockActivity::class.java).apply { val intent = Intent(requireContext(), LockActivity::class.java).apply {
@@ -97,19 +103,15 @@ class SettingsFragment :
} }
lockLauncher.launch(intent) lockLauncher.launch(intent)
} }
"mirrors" -> {
MirrorDialog(requireContext())
.show()
}
"proxy" -> { "proxy" -> {
ProxyDialog(requireContext()) ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
.show()
} }
"user_id" -> { "user_id" -> {
FirebaseCrashlytics.getInstance().recordException(SendLogException())
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
ClipData.newPlainText("user_id", Preferences.get<String>("user_id")) ClipData.newPlainText("user_id", Preferences.get<String>("user_id"))
) )
Toast.makeText(context, R.string.settings_user_id_toast, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
} }
else -> return false else -> return false
} }
@@ -118,11 +120,12 @@ class SettingsFragment :
return true return true
} }
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
with (preference) { with (preference) {
this ?: return false
when (key) { when (key) {
"tag_translation" -> {
updateTranslations()
}
"nomedia" -> { "nomedia" -> {
val create = (newValue as? Boolean) ?: return false val create = (newValue as? Boolean) ?: return false
@@ -156,7 +159,7 @@ class SettingsFragment :
when (key) { when (key) {
"proxy" -> { "proxy" -> {
summary = context?.let { getProxyInfo().type.name } summary = context.let { getProxyInfo().type.name }
} }
"download_folder" -> { "download_folder" -> {
summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath summary = FileX(context, Preferences.get<String>("download_folder")).canonicalPath
@@ -164,6 +167,18 @@ class SettingsFragment :
"download_folder_name" -> { "download_folder_name" -> {
summary = Preferences["download_folder_name", "[-id-] -title-"] summary = Preferences["download_folder_name", "[-id-] -title-"]
} }
"max_concurrent_download" -> {
val newValue = Preferences.get<String>(key).toIntOrNull() ?: 0
if (newValue == 0)
clientBuilder.dispatcher(Dispatcher())
else
clientBuilder.dispatcher((Dispatcher(Executors.newFixedThreadPool(newValue))))
clientHolder = null
client
}
else -> return
} }
} }
} }
@@ -243,14 +258,32 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"proxy" -> { "proxy" -> {
summary = getProxyInfo().type.name summary = getProxyInfo().type.name
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"tag_translation" -> {
this as ListPreference
isEnabled = false
CoroutineScope(Dispatchers.IO).launch {
kotlin.runCatching {
val languages = getAvailableLanguages().distinct().toTypedArray()
entries = languages.map { Locale(it).let { loc -> loc.getDisplayLanguage(loc) } }.toTypedArray()
entryValues = languages
launch(Dispatchers.Main) {
isEnabled = true
}
}
}
onPreferenceChangeListener = this@SettingsFragment
}
"dark_mode" -> { "dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment onPreferenceChangeListener = this@SettingsFragment
} }
@@ -263,7 +296,7 @@ class SettingsFragment :
} }
"oss" -> { "oss" -> {
setOnPreferenceClickListener { setOnPreferenceClickListener {
context?.startActivity(Intent(context, OssLicensesMenuActivity::class.java)) context.startActivity(Intent(context, OssLicensesMenuActivity::class.java))
true true
} }
} }

View File

@@ -22,10 +22,10 @@ import android.content.Context
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
@@ -38,8 +38,6 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import xyz.quaver.floatingsearchview.FloatingSearchView import xyz.quaver.floatingsearchview.FloatingSearchView
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.MenuPopupHelper
import xyz.quaver.floatingsearchview.util.view.MenuView
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
@@ -57,12 +55,12 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null var onFavoriteHistorySwitchClickListener: (() -> Unit)? = null
init { init {
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or searchInputView.imeOptions
searchInputView.addTextChangedListener(this) searchInputView.addTextChangedListener(this)
onSearchListener = this onSearchListener = this
onBindSuggestionCallback = { a, b, c, d, e -> onBindSuggestionCallback = { binding, item, itemPosition ->
onBindSuggestion(a, b, c, d, e) onBindSuggestion(binding.root, binding.leftIcon, binding.body, item, itemPosition)
} }
} }
@@ -113,7 +111,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
) { ) {
when(item) { when(item) {
is TagSuggestion -> { is TagSuggestion -> {
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}" val tag = "${item.n}:${item.s}"
leftIcon?.setImageDrawable( leftIcon?.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
@@ -164,9 +162,7 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
} }
} }
if (item.t == -1) { if (item.t > 0) {
textView?.text = item.s
} else {
(suggestionView as? LinearLayout)?.let { (suggestionView as? LinearLayout)?.let {
val count = it.findViewById<TextView>(R.id.count) val count = it.findViewById<TextView>(R.id.count)
if (count == null) if (count == null)
@@ -217,4 +213,4 @@ class FloatingSearchView @JvmOverloads constructor(context: Context, attrs: Attr
} }
} }
} }
} }

View File

@@ -0,0 +1,462 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.view.NestedScrollingChild;
import androidx.core.view.NestedScrollingChildHelper;
import androidx.core.view.NestedScrollingParent;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.core.widget.TextViewCompat;
import xyz.quaver.pupil.R;
@SuppressWarnings("NullableProblems")
public class MainView extends ViewGroup implements NestedScrollingChild, NestedScrollingParent {
private static final int PAGE_TURN_LAYOUT_SIZE = 48;
private static final int PAGE_TURN_ANIM_DURATION = 500;
private static final int PREV_OFFSET = 64;
private static final int RIPPLE_GIVE = 4;
private final float adjustedPageTurnLayoutSize;
private final float adjustedPrevOffset;
private final float adjustedRippleGive;
final private NestedScrollingParentHelper mNestedScrollingParentHelper;
final private NestedScrollingChildHelper mNestedScrollingChildHelper;
final private Vibrator mVibrator;
private View mTarget;
private TextView mPrev;
private TextView mNext;
private final Paint mRipplePaint = new Paint();
private final Rect mRippleBound = new Rect();
private int mRippleSize = 0;
private final int mRippleTargetSize;
private final ValueAnimator mRippleAnimator = new ValueAnimator();
private int mCurrentOverScroll = 0;
private int mCurrentPage = 1;
private boolean mShowPrev;
private boolean mShowNext;
private OnPageTurnListener mOnPageTurnListener;
public MainView(@NonNull Context context) {
this(context, null);
}
public MainView(@NonNull Context context, AttributeSet attr) {
this(context, attr, 0);
}
public MainView(@NonNull Context context, AttributeSet attr, int defStyle) {
super(context, attr, defStyle);
setWillNotDraw(false);
DisplayMetrics metrics = getResources().getDisplayMetrics();
adjustedPageTurnLayoutSize = PAGE_TURN_LAYOUT_SIZE * metrics.density;
adjustedPrevOffset = PREV_OFFSET * metrics.density;
adjustedRippleGive = RIPPLE_GIVE * metrics.density;
mRippleTargetSize = metrics.widthPixels;
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
mRippleAnimator.addUpdateListener(animation -> {
mRippleSize = (int) animation.getAnimatedValue();
invalidate();
});
mRippleAnimator.setDuration(PAGE_TURN_ANIM_DURATION);
initPageTurnView();
}
public void setCurrentPage(int currentPage, boolean showNext) {
mCurrentPage = currentPage;
mShowPrev = currentPage > 1;
mShowNext = showNext;
mPrev.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage-1));
mNext.setText(getContext().getString(R.string.main_move_to_page, mCurrentPage+1));
}
public void setOnPageTurnListener(OnPageTurnListener listener) {
mOnPageTurnListener = listener;
}
private void initPageTurnView() {
TextView prev = new TextView(getContext());
TextView next = new TextView(getContext());
prev.setGravity(Gravity.CENTER_VERTICAL);
next.setGravity(Gravity.CENTER_VERTICAL);
prev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.navigate_prev, 0, 0, 0);
next.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.navigate_next, 0);
TextViewCompat.setCompoundDrawableTintList(prev, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
TextViewCompat.setCompoundDrawableTintList(next, AppCompatResources.getColorStateList(getContext(), R.color.colorAccent));
prev.setVisibility(View.INVISIBLE);
next.setVisibility(View.INVISIBLE);
mPrev = prev;
mNext = next;
addView(mPrev);
addView(mNext);
setCurrentPage(1, false);
}
private void ensureTarget() {
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mNext) && !child.equals(mPrev)) {
mTarget = child;
break;
}
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0)
return;
if (mTarget == null)
ensureTarget();
if (mTarget == null)
return;
mTarget.layout(
getPaddingLeft(),
getPaddingTop(),
width - getPaddingRight(),
height - getPaddingBottom()
);
final int prevWidth = mPrev.getMeasuredWidth();
mPrev.layout(
width / 2 - prevWidth / 2,
getPaddingTop() + (int) adjustedPrevOffset,
width / 2 + prevWidth / 2,
getPaddingTop() + (int) adjustedPrevOffset + mPrev.getMeasuredHeight()
);
final int nextWidth = mNext.getMeasuredWidth();
mNext.layout(
width / 2 - nextWidth / 2,
height - getPaddingBottom() - mNext.getMeasuredHeight(),
width / 2 + nextWidth / 2,
height - getPaddingBottom()
);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null)
ensureTarget();
if (mTarget == null)
return;
mTarget.measure(
MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)
);
mPrev.measure(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
);
mNext.measure(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec((int) adjustedPageTurnLayoutSize, MeasureSpec.EXACTLY)
);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mCurrentOverScroll == 0)
return;
if (mCurrentOverScroll > 0) {
mRippleBound.set(
getPaddingLeft(),
(int) (getPaddingTop() - adjustedRippleGive),
getMeasuredWidth() - getPaddingRight(),
(int) (getPaddingTop() + adjustedPrevOffset + mPrev.getMeasuredHeight() + adjustedRippleGive)
);
}
if (mCurrentOverScroll < 0) {
final int height = getMeasuredHeight();
mRippleBound.set(
getPaddingLeft(),
(int) (height - getPaddingBottom() - mNext.getMeasuredHeight() - adjustedRippleGive),
getMeasuredWidth() - getPaddingRight(),
height - getPaddingBottom()
);
}
mRipplePaint.reset();
mRipplePaint.setStyle(Paint.Style.FILL);
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_YES:
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_700));
break;
case Configuration.UI_MODE_NIGHT_NO:
mRipplePaint.setColor(ContextCompat.getColor(getContext(), R.color.material_light_blue_300));
break;
}
canvas.drawCircle(
(mRippleBound.left + mRippleBound.right) / 2F,
mCurrentOverScroll > 0 ? mRippleBound.bottom : mRippleBound.top,
mRippleSize,
mRipplePaint
);
}
private void onOverscroll(int overscroll) {
if (mTarget == null)
ensureTarget();
if (mTarget == null)
return;
mCurrentOverScroll = overscroll;
if (overscroll > 0) {
mPrev.setVisibility(View.VISIBLE);
mNext.setVisibility(View.INVISIBLE);
} else if (overscroll < 0) {
mPrev.setVisibility(View.INVISIBLE);
mNext.setVisibility(View.VISIBLE);
} else {
mPrev.setVisibility(View.INVISIBLE);
mNext.setVisibility(View.INVISIBLE);
}
if (Math.abs(overscroll) >= adjustedPageTurnLayoutSize) {
if (!mRippleAnimator.isStarted() && mRippleSize != mRippleTargetSize) {
mVibrator.vibrate(10);
mRippleAnimator.setIntValues(mRippleSize, mRippleTargetSize);
mRippleAnimator.start();
}
} else {
if (!mRippleAnimator.isStarted() && mRippleSize != 0) {
mRippleAnimator.setIntValues(mRippleSize, 0);
mRippleAnimator.start();
}
}
float clippedOverScrollTop = (overscroll > 0 ? 1 : -1) * Math.min(Math.abs(overscroll), adjustedPageTurnLayoutSize);
mTarget.setTranslationY(clippedOverScrollTop);
}
private void onOverscrollEnd(int overscroll) {
if (mTarget == null)
ensureTarget();
if (mTarget == null)
return;
mRippleAnimator.cancel();
mRippleAnimator.setIntValues(mRippleSize, 0);
mRippleAnimator.start();
mPrev.setVisibility(View.INVISIBLE);
mNext.setVisibility(View.INVISIBLE);
ViewCompat.animate(mTarget)
.setDuration(PAGE_TURN_ANIM_DURATION)
.setInterpolator(new DecelerateInterpolator())
.translationY(0);
if (Math.abs(overscroll) > adjustedPageTurnLayoutSize && mOnPageTurnListener != null) {
if (overscroll > 0)
mOnPageTurnListener.onPrev(mCurrentPage-1);
if (overscroll < 0)
mOnPageTurnListener.onNext(mCurrentPage+1);
}
}
// NestedScrollingParent
private int mTotalUnconsumed = 0;
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (mTotalUnconsumed != 0 && dy > 0 == mTotalUnconsumed > 0) {
if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) {
consumed[1] = dy - mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
onOverscroll(mTotalUnconsumed);
}
final int[] parentConsumed = new int[2];
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
final int[] mParentOffsetInWindow = new int[2];
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (mTotalUnconsumed == 0 && ((dy < 0 && !mShowPrev) || (dy > 0 && !mShowNext)))
return;
if (dy != 0) {
mTotalUnconsumed -= dy;
onOverscroll(mTotalUnconsumed);
}
}
@Override
public void onStopNestedScroll(View child) {
mNestedScrollingParentHelper.onStopNestedScroll(child);
if (Math.abs(mTotalUnconsumed) > 0) {
onOverscrollEnd(mTotalUnconsumed);
mTotalUnconsumed = 0;
}
stopNestedScroll();
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
public interface OnPageTurnListener {
void onPrev(int page);
void onNext(int page);
}
}

View File

@@ -0,0 +1,72 @@
package xyz.quaver.pupil.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
class ProgressCard @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, defStyle: Int = R.attr.cardViewStyle) : CardView(context, attr, defStyle) {
enum class Type {
LOADING,
CACHE,
DOWNLOAD
}
var type: Type = Type.LOADING
set(value) {
field = value
when (field) {
Type.LOADING -> R.color.colorAccent
Type.CACHE -> R.color.material_blue_700
Type.DOWNLOAD -> R.color.material_green_a700
}.let {
val color = ContextCompat.getColor(context, it)
DrawableCompat.setTint(binding.progressbar.progressDrawable, color)
}
}
var progress: Int
get() = binding.progressbar.progress
set(value) {
binding.progressbar.progress = value
}
var max: Int
get() = binding.progressbar.max
set(value) {
binding.progressbar.max = value
binding.progressbar.visibility =
if (value == 0)
GONE
else
VISIBLE
}
val binding = ProgressCardViewBinding.inflate(LayoutInflater.from(context), this)
init {
binding.content.setOnClickListener {
performClick()
}
binding.content.setOnLongClickListener {
performLongClick()
}
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (childCount == 0)
super.addView(child, index, params)
else
binding.content.addView(child, index, params)
}
}

View File

@@ -25,6 +25,7 @@ import com.google.android.material.chip.Chip
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.favoriteTags import xyz.quaver.pupil.favoriteTags
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.translations
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
@@ -90,7 +91,7 @@ class TagChip(context: Context, _tag: Tag) : Chip(context) {
text = when (tag.area) { text = when (tag.area) {
"language" -> languages[tag.tag] "language" -> languages[tag.tag]
else -> tag.tag.wordCapitalize() else -> (translations[tag.tag] ?: tag.tag).wordCapitalize()
} }
setEnsureMinTouchTargetSize(false) setEnsureMinTouchTargetSize(false)

View File

@@ -21,8 +21,10 @@ package xyz.quaver.pupil.ui.view
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import kotlinx.coroutines.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
@@ -30,7 +32,7 @@ import xyz.quaver.pupil.types.Tags
class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags { class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSet? = null, attrStyle: Int = R.attr.chipGroupStyle, val tags: Tags = Tags()) : ChipGroup(context, attr, attrStyle), MutableSet<Tag> by tags {
object Defaults { object Defaults {
val maxChipSize = 10 const val maxChipSize = 10
} }
var maxChipSize: Int = Defaults.maxChipSize var maxChipSize: Int = Defaults.maxChipSize
@@ -66,19 +68,27 @@ class TagChipGroup @JvmOverloads constructor(context: Context, attr: AttributeSe
maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize) maxChipSize = attr.getInt(R.styleable.TagChipGroup_maxTag, Defaults.maxChipSize)
} }
private var refreshJob: Job? = null
fun refresh() { fun refresh() {
refreshJob?.cancel()
this.removeAllViews() this.removeAllViews()
tags.take(maxChipSize).forEach { refreshJob = CoroutineScope(Dispatchers.Main).launch {
this.addView(TagChip(context, it).apply { tags.take(maxChipSize).map {
setOnClickListener { CoroutineScope(Dispatchers.Default).async {
onClickListener?.invoke(this.tag) TagChip(context, it).apply {
setOnClickListener {
onClickListener?.invoke(this.tag)
}
}
} }
}) }.forEach {
} addView(it.await())
}
if (maxChipSize > 0 && this.size > maxChipSize) if (maxChipSize > 0 && tags.size > maxChipSize)
addView(moreView) addView(moreView)
}
} }
init { init {

View File

@@ -18,13 +18,15 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.* import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import java.io.File import java.io.File
import java.util.*
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = Collections.synchronizedSet(mutableSetOf())) : MutableSet<T> by set { class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@@ -39,27 +41,26 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
load() load()
} }
@Synchronized
fun load() { fun load() {
synchronized(this) { set.clear()
set.clear() kotlin.runCatching {
kotlin.runCatching { Json.decodeFromString(serializer, file.readText())
Json.decodeFromString(serializer, file.readText()) }.onSuccess {
}.onSuccess { set.addAll(it)
set.addAll(it) }.onFailure {
} FirebaseCrashlytics.getInstance().recordException(it)
} }
} }
@Synchronized
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun save() { fun save() {
synchronized(this) { file.writeText(Json.encodeToString(serializer, set.toList()))
file.writeText(Json.encodeToString(serializer, set.toList()))
}
} }
@Synchronized
override fun add(element: T): Boolean { override fun add(element: T): Boolean {
load()
set.remove(element) set.remove(element)
return set.add(element).also { return set.add(element).also {
@@ -67,9 +68,8 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
} }
} }
@Synchronized
override fun addAll(elements: Collection<T>): Boolean { override fun addAll(elements: Collection<T>): Boolean {
load()
set.removeAll(elements) set.removeAll(elements)
return set.addAll(elements).also { return set.addAll(elements).also {
@@ -77,14 +77,14 @@ class SavedSet <T: Any> (private val file: File, private val any: T, private val
} }
} }
@Synchronized
override fun remove(element: T): Boolean { override fun remove(element: T): Boolean {
load()
return set.remove(element).also { return set.remove(element).also {
save() save()
} }
} }
@Synchronized
override fun clear() { override fun clear() {
set.clear() set.clear()
save() save()

View File

@@ -1,37 +0,0 @@
package xyz.quaver.pupil.util
import android.graphics.Paint
import android.text.style.LineHeightSpan
class SetLineOverlap(private val overlap: Boolean) : LineHeightSpan {
companion object {
private var originalBottom = 15
private var originalDescent = 13
private var overlapSaved = false
}
override fun chooseHeight(
text: CharSequence?,
start: Int,
end: Int,
spanstartv: Int,
lineHeight: Int,
fm: Paint.FontMetricsInt?
) {
fm ?: return
if (overlap) {
if (overlapSaved) {
originalBottom = fm.bottom
originalDescent = fm.descent
overlapSaved = true
}
fm.bottom += fm.top
fm.descent += fm.top
} else {
fm.bottom = originalBottom
fm.descent = originalDescent
overlapSaved = false
}
}
}

View File

@@ -21,39 +21,95 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Request import okhttp3.Request
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.* import xyz.quaver.io.util.*
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.hitomi.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@Serializable @Serializable
data class Metadata( data class OldReader(
val code: String,
val galleryInfo: OldGalleryInfo
)
@Serializable
data class OldGalleryInfo(
val language_localname: String? = null,
val language: String? = null,
val date: String? = null,
val files: List<OldGalleryFiles>,
val id: Int? = null,
val type: String? = null,
val title: String? = null
)
@Serializable
data class OldGalleryFiles(
val width: Int,
val hash: String,
val haswebp: Int = 0,
val name: String,
val height: Int,
val hasavif: Int = 0,
val hasavifsmalltn: Int? = 0
)
@Serializable
data class OldMetadata(
var galleryBlock: GalleryBlock? = null, var galleryBlock: GalleryBlock? = null,
var reader: Reader? = null, var reader: OldReader? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
fun copy(): Metadata = Metadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } }) fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
@Serializable
data class Metadata(
var galleryBlock: GalleryBlock? = null,
var galleryInfo: GalleryInfo? = null,
var imageList: MutableList<String?>? = null
) {
constructor(old: OldMetadata) : this(
old.galleryBlock,
old.reader?.galleryInfo?.let { oldGalleryInfo ->
GalleryInfo(
oldGalleryInfo.id.toString(),
oldGalleryInfo.title ?: "",
null,
oldGalleryInfo.language,
oldGalleryInfo.type ?: "",
oldGalleryInfo.date ?: "",
files = oldGalleryInfo.files.map {
GalleryFiles(
it.width,
it.hash,
it.haswebp,
it.name,
it.height,
it.hasavif,
it.hasavifsmalltn
)
}
)
},
old.imageList
)
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
} }
class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) { class Cache private constructor(context: Context, val galleryID: Int) : ContextWrapper(context) {
companion object { companion object {
val instances = ConcurrentHashMap<Int, Cache>() val instances = ConcurrentHashMap<Int, Cache>()
@@ -74,10 +130,14 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
} }
var metadata = kotlin.runCatching { var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let { findFile(".metadata")?.readText()?.let { metadata ->
Json.decodeFromString<Metadata>(it) kotlin.runCatching {
Json.decodeFromString<Metadata>(metadata)
}.getOrElse {
Metadata(json.decodeFromString<OldMetadata>(metadata))
}
} }
}.getOrNull() ?: Metadata() }.onFailure { it.printStackTrace() }.getOrNull() ?: Metadata()
val downloadFolder: FileX? val downloadFolder: FileX?
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID) get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
@@ -110,27 +170,13 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
} }
suspend fun getGalleryBlock(): GalleryBlock? { suspend fun getGalleryBlock(): GalleryBlock? {
val sources = listOf(
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
return metadata.galleryBlock return metadata.galleryBlock
?: withContext(Dispatchers.IO) { ?: withContext(Dispatchers.IO) {
var galleryBlock: GalleryBlock? = null try {
getGalleryBlock(galleryID).also {
for (source in sources) { setMetadata { metadata -> metadata.galleryBlock = it }
galleryBlock = try { }
source.invoke() } catch (e: Exception) { return@withContext null }
} catch (e: Exception) { null }
if (galleryBlock != null)
break
}
galleryBlock?.also {
setMetadata { metadata -> metadata.galleryBlock = it }
}
} }
} }
@@ -141,6 +187,7 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
kotlin.runCatching { kotlin.runCatching {
val request = Request.Builder() val request = Request.Builder()
.url(it) .url(it)
.header("Referer", "https://hitomi.la/")
.build() .build()
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() } client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
@@ -154,41 +201,21 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}.getOrNull()?.uri } }.getOrNull()?.uri }
} } ?: Uri.EMPTY } } ?: Uri.EMPTY
suspend fun getReader(): Reader? { suspend fun getGalleryInfo(): GalleryInfo? {
val mirrors = Preferences.get<String>("mirrors").let { if (it.isEmpty()) emptyList() else it.split('>') }
val sources = mapOf( return metadata.galleryInfo
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap{ o1, o2 -> mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name) }
else
it
}
return metadata.reader
?: withContext(Dispatchers.IO) { ?: withContext(Dispatchers.IO) {
var reader: Reader? = null try {
getGalleryInfo(galleryID).also {
setMetadata { metadata ->
metadata.galleryInfo = it
for (source in sources) { if (metadata.imageList == null)
reader = try { metadata.imageList = MutableList(it.files.size) { null }
source.value.invoke() }
} catch (e: Exception) {
null
}
if (reader != null)
break
}
reader?.also {
setMetadata { metadata ->
metadata.reader = it
if (metadata.imageList == null)
metadata.imageList = MutableList(reader.galleryInfo.files.size) { null }
} }
} catch (e: Exception) {
null
} }
} }
} }
@@ -197,11 +224,12 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.imageList?.getOrNull(index)?.let { findFile(it) } metadata.imageList?.getOrNull(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
fun putImage(index: Int, fileName: String, data: ByteArray) { suspend fun putImage(index: Int, fileName: String, data: ByteArray) = coroutineScope {
val file = cacheFolder.getChild(fileName) val file = cacheFolder.getChild(fileName)
if (!file.exists()) if (!file.exists())
file.createNewFile() file.createNewFile()
file.writeBytes(data) file.writeBytes(data)
setMetadata { metadata -> metadata.imageList!![index] = fileName } setMetadata { metadata -> metadata.imageList!![index] = fileName }
} }

View File

@@ -20,8 +20,9 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Log import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -47,14 +48,12 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!) val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
val downloadFolder: FileX val downloadFolder: FileX
get() = { get() = kotlin.runCatching {
kotlin.runCatching { FileX(this, Preferences.get<String>("download_folder"))
FileX(this, Preferences.get<String>("download_folder")) }.getOrElse {
}.getOrElse { Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
Preferences["download_folder"] = defaultDownloadFolder.uri.toString() defaultDownloadFolder
defaultDownloadFolder }
}
}.invoke()
private var prevDownloadFolder: FileX? = null private var prevDownloadFolder: FileX? = null
private var downloadFolderMapInstance: MutableMap<Int, String>? = null private var downloadFolderMapInstance: MutableMap<Int, String>? = null
@@ -63,30 +62,28 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
get() { get() {
if (prevDownloadFolder != downloadFolder) { if (prevDownloadFolder != downloadFolder) {
prevDownloadFolder = downloadFolder prevDownloadFolder = downloadFolder
downloadFolderMapInstance = { downloadFolderMapInstance = run {
val file = downloadFolder.getChild(".download") val file = downloadFolder.getChild(".download")
val data = if (file.exists()) val data = if (file.exists())
kotlin.runCatching { kotlin.runCatching {
file.readText()?.let { Json.decodeFromString<MutableMap<Int, String>>(it) } file.readText()?.let{ Json.decodeFromString<MutableMap<Int, String>>(it) }
}.onFailure { file.delete() }.getOrNull() }.onFailure { file.delete() }.getOrNull()
else else
null null
data ?: run {
data ?: {
file.createNewFile() file.createNewFile()
mutableMapOf<Int, String>() mutableMapOf()
}.invoke() }
}.invoke() }
} }
return downloadFolderMapInstance!! return downloadFolderMapInstance ?: mutableMapOf()
} }
@Synchronized @Synchronized
fun isDownloading(galleryID: Int): Boolean { fun isDownloading(galleryID: Int): Boolean {
val isThisGallery: (Call) -> Boolean = { (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID } val isThisGallery: (Call) -> Boolean = { !it.isCanceled && (it.request().tag() as? DownloadService.Tag)?.galleryID == galleryID }
return downloadFolderMap.containsKey(galleryID) return downloadFolderMap.containsKey(galleryID)
&& client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) } && client.dispatcher().let { it.queuedCalls().any(isThisGallery) || it.runningCalls().any(isThisGallery) }
@@ -96,23 +93,19 @@ class DownloadManager private constructor(context: Context) : ContextWrapper(con
fun getDownloadFolder(galleryID: Int): FileX? = fun getDownloadFolder(galleryID: Int): FileX? =
downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) } downloadFolderMap[galleryID]?.let { downloadFolder.getChild(it) }
@Synchronized fun addDownloadFolder(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
fun addDownloadFolder(galleryID: Int) { val name = Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
val name = runBlocking { ?.formatDownloadFolder() ?: return@launch
Cache.getInstance(this@DownloadManager, galleryID).getGalleryBlock()
}?.formatDownloadFolder() ?: return
val folder = downloadFolder.getChild(name) val folder = downloadFolder.getChild(name)
if (folder.exists()) downloadFolderMap[galleryID] = name
return
folder.mkdir()
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() } downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap)) downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
if (folder.exists()) return@launch
folder.mkdir()
} }
@Synchronized @Synchronized

View File

@@ -55,10 +55,12 @@ fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
while (cacheSize.invoke() > limit/2) { while (cacheSize.invoke() > limit/2) {
val caches = cacheFolder.list() ?: return@withLock val caches = cacheFolder.list() ?: return@withLock
(histories.firstOrNull { synchronized(histories) {
caches.contains(it.toString()) && !downloadManager.isDownloading(it) (histories.firstOrNull {
} ?: return@withLock).let { caches.contains(it.toString()) && !downloadManager.isDownloading(it)
Cache.delete(context, it) } ?: return@withLock).let {
Cache.delete(context, it)
}
} }
} }
} }

View File

@@ -19,18 +19,13 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import com.google.firebase.crashlytics.FirebaseCrashlytics
import android.content.Intent import kotlinx.serialization.json.*
import android.os.Build
import androidx.core.content.ContextCompat
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import xyz.quaver.Code import xyz.quaver.pupil.hitomi.GalleryBlock
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.pupil.hitomi.imageUrlFromImage
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.createImgList
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -40,7 +35,7 @@ fun String.wordCapitalize() : String {
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
for (word in this.split(" ")) for (word in this.split(" "))
result.add(word.capitalize(Locale.US)) result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() })
return result.joinToString(" ") return result.joinToString(" ")
} }
@@ -82,7 +77,8 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
val formatMap = mapOf<String, GalleryBlock.() -> (String)>( val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
"-id-" to { id.toString() }, "-id-" to { id.toString() },
"-title-" to { title }, "-title-" to { title },
"-artist-" to { artists.joinToString() } "-artist-" to { if (artists.isNotEmpty()) artists.joinToString() else "N/A" },
"-group-" to { if (groups.isNotEmpty()) groups.joinToString() else "N/A" }
// TODO // TODO
) )
/** /**
@@ -102,30 +98,38 @@ fun GalleryBlock.formatDownloadFolderTest(format: String): String =
} }
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127) }.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
val Reader.requestBuilders: List<Request.Builder> suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
get() { val galleryID = this.id.toIntOrNull() ?: 0
val galleryID = this.galleryInfo.id ?: 0 return this.files.map {
val lowQuality = Preferences["low_quality", true] Request.Builder()
.url(
return when(code) { runCatching {
Code.HITOMI -> { imageUrlFromImage(galleryID, it, false)
this.galleryInfo.files.map {
Request.Builder()
.url(imageUrlFromImage(galleryID, it, !lowQuality))
.header("Referer", getReferer(galleryID))
} }
} .onFailure {
Code.HIYOBI -> { FirebaseCrashlytics.getInstance().recordException(it)
createImgList(galleryID, this, lowQuality).map {
Request.Builder()
.url(it.path)
} }
} .getOrDefault("https://a/")
} )
.header("Referer", "https://hitomi.la/")
} }
}
fun String.ellipsize(n: Int): String = fun String.ellipsize(n: Int): String =
if (this.length > n) if (this.length > n)
this.slice(0 until n) + "" this.slice(0 until n) + ""
else else
this this
operator fun JsonElement.get(index: Int) =
this.jsonArray[index]
operator fun JsonElement.get(tag: String) =
this.jsonObject[tag]
fun JsonElement.getOrNull(tag: String) = kotlin.runCatching {
this.jsonObject.getOrDefault(tag, null)
}.getOrNull()
val JsonElement.content
get() = this.jsonPrimitive.contentOrNull

View File

@@ -0,0 +1,68 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request
import xyz.quaver.pupil.client
import java.io.IOException
import java.util.*
private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags"
private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/"
var translations: Map<String, String> = run {
updateTranslations()
emptyMap()
}
private set
@Suppress("BlockingMethodInNonBlockingContext")
fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch {
translations = emptyMap()
kotlin.runCatching {
translations = Json.decodeFromString<Map<String, String>>(client.newCall(
Request.Builder()
.url(contentURL + "${Preferences["tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json")
.build()
).execute().also { if (it.code() != 200) return@launch }.body()?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() }
}
}
fun getAvailableLanguages(): List<String> {
val languages = Locale.getISOLanguages()
val json = Json.parseToJsonElement(client.newCall(
Request.Builder()
.url(filesURL)
.build()
).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.string() } ?: return emptyList())
return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull {
val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' }
languages.firstOrNull { code -> code.equals(name, ignoreCase = true) }
} ?: emptyList())
}

View File

@@ -18,48 +18,23 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.util.Base64
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.pupil.*
import xyz.quaver.hitomi.Reader import xyz.quaver.pupil.types.Tag
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.hitomi.getReader
import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild
import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeBytes
import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.Metadata
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@@ -181,10 +156,10 @@ fun checkUpdate(context: Context, force: Boolean = false) {
Preferences["update_download_id"] = it Preferences["update_download_id"] = it
} }
} }
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ -> setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
if (!force) if (!force)
preferences.edit() preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000) .putLong("ignore_update_until", System.currentTimeMillis() + 86400000)
.apply() .apply()
} }
} }
@@ -195,7 +170,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
} }
} }
fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List<Int>) -> Unit)? = null) { fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Int) -> Unit)? = null) {
if (!URLUtil.isValidUrl(url)) { if (!URLUtil.isValidUrl(url)) {
onFailure?.invoke(IllegalArgumentException()) onFailure?.invoke(IllegalArgumentException())
return return
@@ -213,123 +188,22 @@ fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
kotlin.runCatching { kotlin.runCatching {
Json.decodeFromString<List<Int>>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let { val data = Json.parseToJsonElement(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]")
favorites.addAll(it)
onSuccess?.invoke(it) when (data) {
is JsonArray -> favorites.addAll(data.map { it.jsonPrimitive.int })
is JsonObject -> {
val newFavorites = data["favorites"]?.let { Json.decodeFromJsonElement<List<Int>>(it) }.orEmpty()
val newFavoriteTags = data["favorite_tags"]?.let { Json.decodeFromJsonElement<List<Tag>>(it) }.orEmpty()
favorites.addAll(newFavorites)
favoriteTags.addAll(newFavoriteTags)
onSuccess?.invoke(favorites.size + favoriteTags.size)
}
else -> error("data is neither JsonArray or JsonObject")
} }
}.onFailure { onFailure?.invoke(it) } }.onFailure { onFailure?.invoke(it) }
} }
}) })
}
private var job: Job? = null
private val receiver = object: BroadcastReceiver() {
val ACTION_CANCEL = "ACTION_IMPORT_CANCEL"
override fun onReceive(context: Context?, intent: Intent?) {
context ?: return
when (intent?.action) {
ACTION_CANCEL -> {
job?.cancel()
NotificationManagerCompat.from(context).cancel(R.id.notification_id_import)
context.unregisterReceiver(this)
}
}
}
}
@SuppressLint("RestrictedApi")
fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) })
val notificationManager = NotificationManagerCompat.from(this)
val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel),
PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT)
).build()
val notification = NotificationCompat.Builder(this, "import")
.setContentTitle(getText(R.string.import_old_galleries_notification))
.setProgress(0, 0, true)
.addAction(action)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
DownloadService.cancel(this)
job?.cancel()
job = CoroutineScope(Dispatchers.IO).launch {
val downloadFolders = downloadFolder.listFiles { folder ->
folder.isDirectory && !downloadFolderMap.values.contains(folder.name)
}?.map {
if (it !is FileX)
FileX(this@migrate, it)
else
it
}
if (downloadFolders.isNullOrEmpty()) return@launch
downloadFolders.forEachIndexed { index, folder ->
notification
.setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size))
.setProgress(index, downloadFolders.size, false)
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
val metadata = kotlin.runCatching {
folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it).jsonObject }
}.getOrNull()
val galleryID = folder.name.toIntOrNull() ?: return@runCatching
val galleryBlock: GalleryBlock? = kotlin.runCatching {
metadata?.get("galleryBlock")?.let { Json.decodeFromJsonElement<GalleryBlock>(it) }
}.getOrNull() ?: getGalleryBlock(galleryID)
val reader: Reader? = kotlin.runCatching {
metadata?.get("reader")?.let { Json.decodeFromJsonElement<Reader>(it) }
}.getOrNull() ?: getReader(galleryID)
metadata?.get("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail ->
val file = folder.getChild(".thumbnail").also {
if (it.exists())
it.delete()
it.createNewFile()
}
file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT))
}
val list: MutableList<String?> =
MutableList(reader!!.galleryInfo.files.size) { null }
folder.listFiles { file ->
file?.nameWithoutExtension?.let {
Regex("""\d{5}""").matches(it) && it.toIntOrNull() != null
} == true
}?.forEach {
list[it.nameWithoutExtension.toInt()] = it.name
}
folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText(
Json.encodeToString(Metadata(galleryBlock, reader, list))
)
synchronized(Cache) {
Cache.delete(this@migrate, galleryID)
}
downloadFolderMap[galleryID] = folder.name
downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) }
}
}
notification
.setContentText(getText(R.string.import_old_galleries_notification_done))
.setProgress(0, 0, false)
.setOngoing(false)
.mActions.clear()
notificationManager.notify(R.id.notification_id_import, notification.build())
kotlin.runCatching {
unregisterReceiver(receiver)
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

View File

@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/> <path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</vector> </vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<group android:pivotX="12" android:scaleX="-1">
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</group>
</vector>

View File

@@ -1,147 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="64dp"
android:visibility="invisible"
android:background="@android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</com.google.android.material.appbar.AppBarLayout>
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/main_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/main_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView
android:id="@+id/main_noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/main_no_result"
android:linksClickable="true"
android:visibility="invisible"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_random"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<xyz.quaver.pupil.ui.view.FloatingSearchView
android:id="@+id/main_searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:searchBarMarginLeft="6dp"
app:searchBarMarginRight="6dp"
app:searchBarMarginTop="6dp"
app:searchHint="@string/search_hint"
app:suggestionAnimDuration="250"
app:showSearchKey="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:dismissOnOutsideTouch="true"
app:close_search_on_keyboard_dismiss="false"
tools:ignore="NewApi" />
</RelativeLayout>

View File

@@ -28,7 +28,7 @@
tools:ignore="Autofill" tools:ignore="Autofill"
android:inputType="text" android:inputType="text"
android:hint="@string/settings_default_query" android:hint="@string/settings_default_query"
android:id="@+id/default_query_dialog_edittext" android:id="@+id/edittext"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -36,10 +36,10 @@
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout <LinearLayout
android:id="@+id/default_query_dialog_language_layout" android:id="@+id/language_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_edittext" app:layout_constraintTop_toBottomOf="@id/edittext"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -49,14 +49,14 @@
android:text="@string/default_query_dialog_language"/> android:text="@string/default_query_dialog_language"/>
<Spinner <Spinner
android:id="@+id/default_query_dialog_language_selector" android:id="@+id/language_selector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/default_query_dialog_BL_layout" android:id="@+id/BL_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -64,7 +64,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_language_layout" app:layout_constraintTop_toBottomOf="@id/language_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -75,14 +75,14 @@
android:text="@string/default_query_dialog_filter_BL"/> android:text="@string/default_query_dialog_filter_BL"/>
<CheckBox <CheckBox
android:id="@+id/default_query_dialog_BL_checkbox" android:id="@+id/BL_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/default_query_dialog_guro_layout" android:id="@+id/guro_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@@ -90,7 +90,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_BL_layout" app:layout_constraintTop_toBottomOf="@id/BL_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -101,7 +101,7 @@
android:text="@string/default_query_dialog_filter_guro"/> android:text="@string/default_query_dialog_filter_guro"/>
<CheckBox <CheckBox
android:id="@+id/default_query_dialog_guro_checkbox" android:id="@+id/guro_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@@ -115,7 +115,7 @@
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout" app:layout_constraintTop_toBottomOf="@id/guro_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
@@ -126,7 +126,7 @@
android:text="@string/default_query_dialog_filter_loli"/> android:text="@string/default_query_dialog_filter_loli"/>
<CheckBox <CheckBox
android:id="@+id/default_query_dialog_loli_checkbox" android:id="@+id/loli_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

View File

@@ -19,14 +19,13 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gallery_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/gallery_toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -41,34 +40,34 @@
android:padding="8dp"> android:padding="8dp">
<com.github.piasy.biv.view.BigImageView <com.github.piasy.biv.view.BigImageView
android:id="@+id/gallery_cover" android:id="@+id/cover"
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/gallery_title" app:layout_constraintRight_toLeftOf="@id/title"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/gallery_title" android:id="@+id/title"
style="@style/TextAppearance.AppCompat.Headline" style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover" app:layout_constraintLeft_toRightOf="@id/cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
<TextView <TextView
style="@style/TextAppearance.AppCompat.Medium" style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/gallery_artist" android:id="@+id/artist"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/gallery_title" app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintLeft_toRightOf="@id/gallery_cover" app:layout_constraintLeft_toRightOf="@id/cover"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -76,15 +75,15 @@
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gallery_artist" app:layout_constraintTop_toBottomOf="@id/artist"
app:layout_constraintBottom_toTopOf="@id/gallery_type"/> app:layout_constraintBottom_toTopOf="@id/type"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/gallery_type" android:id="@+id/type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover" app:layout_constraintLeft_toRightOf="@id/cover"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/> android:layout_marginStart="8dp"/>
@@ -100,7 +99,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
android:id="@+id/gallery_contents" android:id="@+id/contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"/> android:orientation="vertical"/>
@@ -112,7 +111,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<ProgressBar <ProgressBar
android:id="@+id/gallery_progressbar" android:id="@+id/progressbar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@@ -123,11 +122,11 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/gallery_fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
app:layout_anchor="@id/gallery_toolbar" app:layout_anchor="@id/toolbar"
app:layout_anchorGravity="bottom|end"/> app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -25,14 +25,14 @@
android:padding="8dp"> android:padding="8dp">
<TextView <TextView
android:id="@+id/gallery_details" android:id="@+id/type"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1" style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"/> android:textColor="@color/colorAccent"/>
<LinearLayout <LinearLayout
android:id="@+id/gallery_details_contents" android:id="@+id/contents"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"/> android:orientation="vertical"/>

View File

@@ -25,7 +25,7 @@
android:layout_margin="8dp"> android:layout_margin="8dp">
<com.tbuonomo.viewpagerdotsindicator.DotsIndicator <com.tbuonomo.viewpagerdotsindicator.DotsIndicator
android:id="@+id/gallery_dotindicator" android:id="@+id/dotindicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"

View File

@@ -26,13 +26,13 @@
<TextView <TextView
style="@style/TextAppearance.MaterialComponents.Body2" style="@style/TextAppearance.MaterialComponents.Body2"
android:id="@+id/gallery_details_type" android:id="@+id/type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="8dp"/> android:paddingBottom="8dp"/>
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/gallery_details_tags" android:id="@+id/tags"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipSpacingVertical="4dp"/> app:chipSpacingVertical="4dp"/>

View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<xyz.quaver.pupil.ui.view.ProgressCard
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/galleryblock_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="true"
app:cardCornerRadius="4dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:cardUseCompatPadding="true"
tools:ignore="RtlHardcoded">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.github.piasy.biv.view.BigImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="0dp"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
android:clickable="false"
app:layout_constraintHeight_default="spread"
app:layout_constraintHeight_min="200dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/barrier"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title" />
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<xyz.quaver.pupil.ui.view.TagChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="galleryblock_thumbnail, galleryblock_tag_group"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@id/barrier"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/galleryblock_pagecount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:srcCompat="@drawable/ic_star_empty"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</xyz.quaver.pupil.ui.view.ProgressCard>

View File

@@ -1,235 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
android:clipChildren="true"
tools:ignore="RtlHardcoded">
<com.daimajia.swipe.SwipeLayout
android:id="@+id/galleryblock_swipe_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:drag_edge="right"
app:show_mode="pull_out">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/galleryblock_download"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
<TextView
android:id="@+id/galleryblock_delete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_red_dark"
android:textColor="@android:color/white"
android:text="@string/main_delete"
android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/galleryblock_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<FrameLayout
android:id="@+id/galleryblock_progressbar_layout"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-4dp"
android:layout_marginTop="-4dp"
android:progress="50"
android:layout_gravity="center_vertical"/>
<ImageView
android:id="@+id/galleryblock_progress_complete"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/>
</FrameLayout>
<com.github.piasy.biv.view.BigImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="0dp"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintHeight_default="spread"
app:layout_constraintHeight_min="200dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"
app:layout_constraintBottom_toTopOf="@id/barrier"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar_layout"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" />
<xyz.quaver.pupil.ui.view.TagChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="galleryblock_thumbnail, galleryblock_tag_group"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
app:layout_constraintTop_toBottomOf="@id/barrier"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/galleryblock_pagecount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent" />
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:srcCompat="@drawable/ic_star_empty"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.daimajia.swipe.SwipeLayout>
</androidx.cardview.widget.CardView>

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:id="@+id/icon_next"
android:contentDescription="@string/page_indicator_placeholder"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
app:tint="@color/colorAccent"
android:rotation="180"/>
<TextView
android:id="@+id/text_next"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:id="@+id/icon_prev"
android:contentDescription="@string/page_indicator_placeholder"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
app:tint="@color/colorAccent"
android:rotation="180"/>
<TextView
android:id="@+id/text_prev"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -41,7 +41,7 @@
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"> app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint" android:id="@+id/fingerprint_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint" app:srcCompat="@drawable/fingerprint"
@@ -64,7 +64,7 @@
android:gravity="center"> android:gravity="center">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_pattern" android:id="@+id/pattern_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"
@@ -73,7 +73,7 @@
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_pin" android:id="@+id/pin_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"
@@ -84,7 +84,7 @@
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_password" android:id="@+id/password_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tint="@null" app:tint="@null"

View File

@@ -21,17 +21,18 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_drawer_layout" android:id="@+id/drawer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:openDrawer="start"> tools:openDrawer="start">
<include layout="@layout/activity_main_content" <include android:id="@+id/contents"
layout="@layout/main_activity_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView
android:id="@+id/main_nav_view" android:id="@+id/nav_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<xyz.quaver.pupil.ui.view.MainView
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller
android:layout_width="match_parent"
android:layout_height="match_parent"
app:handleDrawable="@drawable/thumb"
app:handleHasFixedSize="true"
app:handleHeight="72dp"
app:handleWidth="24dp"
app:disableTrack="true"
app:hideHandleAfter="1000"
app:trackMarginStart="64dp"
app:addLastItemPadding="true"
app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="64dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
</xyz.quaver.pupil.ui.view.MainView>
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyle"
android:id="@+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView
android:id="@+id/noresult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/main_no_result"
android:linksClickable="true"
android:visibility="invisible"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/cancel_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_cancel"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/jump_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/random_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_fab_random"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/id_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
<xyz.quaver.pupil.ui.view.FloatingSearchView
android:id="@+id/searchview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:searchBarMarginLeft="6dp"
app:searchBarMarginRight="6dp"
app:searchBarMarginTop="6dp"
app:searchHint="@string/search_hint"
app:suggestionAnimDuration="250"
app:showSearchKey="true"
app:leftActionMode="showHamburger"
app:menu="@menu/main"
app:dismissOnOutsideTouch="true"
app:close_search_on_keyboard_dismiss="false" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -25,7 +25,7 @@
<TextView <TextView
style="?android:textAppearanceLarge" style="?android:textAppearanceLarge"
android:id="@+id/dialog_title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/reader_go_to_page" android:text="@string/reader_go_to_page"
@@ -33,20 +33,20 @@
app:layout_constraintStart_toStartOf="parent"/> app:layout_constraintStart_toStartOf="parent"/>
<NumberPicker <NumberPicker
android:id="@+id/dialog_number_picker" android:id="@+id/number_picker"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/dialog_title" app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<Button <Button
android:id="@+id/dialog_ok" android:id="@+id/ok_button"
style="?borderlessButtonStyle" style="?borderlessButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@android:string/ok" android:text="@android:string/ok"
app:layout_constraintTop_toBottomOf="@id/dialog_number_picker" app:layout_constraintTop_toBottomOf="@id/number_picker"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>

View File

@@ -25,7 +25,7 @@
tools:context=".ui.fragment.PatternLockFragment"> tools:context=".ui.fragment.PatternLockFragment">
<com.andrognito.patternlockview.PatternLockView <com.andrognito.patternlockview.PatternLockView
android:id="@+id/lock_pattern_view" android:id="@+id/pattern_lock_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="androidx.cardview.widget.CardView">
<com.daimajia.swipe.SwipeLayout
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:drag_edge="right"
app:show_mode="pull_out">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
<TextView
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_red_dark"
android:textColor="@android:color/white"
android:text="@string/main_delete"
android:foreground="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
tools:ignore="UnusedAttribute" />
</LinearLayout>
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical">
<androidx.core.widget.ContentLoadingProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:progress="50"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
</com.daimajia.swipe.SwipeLayout>
</merge>

View File

@@ -24,7 +24,7 @@
android:padding="16dp"> android:padding="16dp">
<TextView <TextView
android:id="@+id/proxy_title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="16dp" android:paddingBottom="16dp"
@@ -35,46 +35,46 @@
app:layout_constraintLeft_toLeftOf="parent"/> app:layout_constraintLeft_toLeftOf="parent"/>
<TextView <TextView
android:id="@+id/proxy_type_text" android:id="@+id/type_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_title" app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
android:text="@string/proxy_dialog_type" android:text="@string/proxy_dialog_type"
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/> android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
<Spinner <Spinner
android:id="@+id/proxy_type_selector" android:id="@+id/type_selector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_type_text"/> app:layout_constraintTop_toBottomOf="@id/type_text"/>
<TextView <TextView
android:id="@+id/proxy_server_text" android:id="@+id/server_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_type_selector" app:layout_constraintTop_toBottomOf="@id/type_selector"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
android:text="@string/proxy_dialog_server" android:text="@string/proxy_dialog_server"
android:textAppearance="?android:attr/listSeparatorTextViewStyle"/> android:textAppearance="?android:attr/listSeparatorTextViewStyle"/>
<LinearLayout <LinearLayout
android:id="@+id/proxy_address_layout" android:id="@+id/address_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_server_text"> app:layout_constraintTop_toBottomOf="@id/server_text">
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_addr" android:id="@+id/addr"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="2" android:layout_weight="2"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/proxy_dialog_addr_hint"/> android:hint="@string/proxy_dialog_addr_hint"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_port" android:id="@+id/port"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -83,39 +83,39 @@
</LinearLayout> </LinearLayout>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_username" android:id="@+id/username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_address_layout" app:layout_constraintTop_toBottomOf="@id/address_layout"
android:hint="@string/proxy_dialog_username_hint" android:hint="@string/proxy_dialog_username_hint"
android:enabled="false"/> android:enabled="false"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
android:id="@+id/proxy_password" android:id="@+id/password"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/proxy_username" app:layout_constraintTop_toBottomOf="@id/username"
android:hint="@string/proxy_dialog_password_hint" android:hint="@string/proxy_dialog_password_hint"
android:enabled="false"/> android:enabled="false"/>
<Button <Button
android:id="@+id/proxy_cancel" android:id="@+id/cancel_button"
style="?borderlessButtonStyle" style="?borderlessButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@android:string/cancel" android:text="@android:string/cancel"
app:layout_constraintTop_toBottomOf="@id/proxy_password" app:layout_constraintTop_toBottomOf="@id/password"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/proxy_ok" app:layout_constraintEnd_toStartOf="@id/ok_button"
app:layout_constraintRight_toLeftOf="@id/proxy_ok"/> app:layout_constraintRight_toLeftOf="@id/ok_button"/>
<Button <Button
android:id="@+id/proxy_ok" android:id="@+id/ok_button"
style="?borderlessButtonStyle" style="?borderlessButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@android:string/ok" android:text="@android:string/ok"
app:layout_constraintTop_toBottomOf="@id/proxy_password" app:layout_constraintTop_toBottomOf="@id/password"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"/> app:layout_constraintRight_toRightOf="parent"/>

View File

@@ -18,7 +18,6 @@
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reader_layout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -40,7 +39,7 @@
app:popupDrawable="@android:color/transparent"> app:popupDrawable="@android:color/transparent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/reader_recyclerview" android:id="@+id/recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
@@ -56,13 +55,13 @@
android:layout_margin="8dp"/> android:layout_margin="8dp"/>
<ProgressBar <ProgressBar
android:id="@+id/reader_download_progressbar" android:id="@+id/download_progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp"/> android:layout_height="4dp"/>
<com.github.clans.fab.FloatingActionMenu <com.github.clans.fab.FloatingActionMenu
android:id="@+id/reader_fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
@@ -70,7 +69,7 @@
app:menu_colorNormal="@color/colorAccent"> app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_download" android:id="@+id/download_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_download" app:srcCompat="@drawable/ic_download"
@@ -78,7 +77,7 @@
app:fab_size="mini"/> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_retry" android:id="@+id/retry_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/refresh" app:srcCompat="@drawable/refresh"
@@ -86,7 +85,7 @@
app:fab_size="mini"/> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_auto" android:id="@+id/auto_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/eye_white" app:srcCompat="@drawable/eye_white"
@@ -94,7 +93,7 @@
app:fab_size="mini"/> app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton <com.github.clans.fab.FloatingActionButton
android:id="@+id/reader_fab_fullscreen" android:id="@+id/fullscreen_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen" app:srcCompat="@drawable/ic_fullscreen"

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="android.widget.FrameLayout">
<TextView
android:id="@+id/prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|center"
android:maxLines="1"
android:ellipsize="end"
app:drawableStartCompat="@drawable/navigate_prev"
app:drawableLeftCompat="@drawable/navigate_prev"
app:drawableTint="@color/colorAccent" />
<TextView
android:id="@+id/next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:maxLines="1"
android:ellipsize="end"
app:drawableEndCompat="@drawable/navigate_next"
app:drawableRightCompat="@drawable/navigate_next"
app:drawableTint="@color/colorAccent" />
</merge>

View File

@@ -48,7 +48,7 @@
<string name="main_jump_title">ページ移動</string> <string name="main_jump_title">ページ移動</string>
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string> <string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
<string name="unable_to_connect">hitomi.laに接続できません</string> <string name="unable_to_connect">hitomi.laに接続できません</string>
<string name="main_move">%1$dページへ移動</string> <string name="main_move_to_page">%1$dページへ移動</string>
<string name="settings_clear_downloads">ダウンロード削除</string> <string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string> <string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string> <string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
@@ -73,7 +73,7 @@
<string name="main_menu_sort">ソート</string> <string name="main_menu_sort">ソート</string>
<string name="main_menu_sort_newest">投稿日時順</string> <string name="main_menu_sort_newest">投稿日時順</string>
<string name="main_menu_sort_popular">人気順</string> <string name="main_menu_sort_popular">人気順</string>
<string name="ignore_update">無視</string> <string name="ignore">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string> <string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="settings_dark_mode_title">ダークモード</string> <string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string> <string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
@@ -128,7 +128,7 @@
<string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string> <string name="settings_lock_fingerprint_prompt_subtitle">こうかはばつぐんだ!</string>
<string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string> <string name="default_query_dialog_filter_loli">登場人物を全て18歳以上にする</string>
<string name="settings_user_id">ユーザーID</string> <string name="settings_user_id">ユーザーID</string>
<string name="settings_user_id_toast">ユーザーIDをクリップボードにコピーしました</string> <string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="reader_fab_retry">リトライ</string> <string name="reader_fab_retry">リトライ</string>
<string name="reader_fab_auto">まばたき検知スクロール</string> <string name="reader_fab_auto">まばたき検知スクロール</string>
<string name="search_all">全てのギャラリーを対象に検索</string> <string name="search_all">全てのギャラリーを対象に検索</string>
@@ -151,5 +151,11 @@
<string name="no_camera">この機器には前面カメラが装着されていません</string> <string name="no_camera">この機器には前面カメラが装着されていません</string>
<string name="error">エラー</string> <string name="error">エラー</string>
<string name="settings_cache_limit">キャッシュサイズ制限</string> <string name="settings_cache_limit">キャッシュサイズ制限</string>
<string name="settings_cache_unlimited">制限なし</string> <string name="unlimited">制限なし</string>
<string name="settings_tag_translation">タグ言語</string>
<string name="settings_tag_translation_message">Githubにて翻訳に参加できます</string>
<string name="settings_max_concurrent_download">並列ダウンロード</string>
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
</resources> </resources>

View File

@@ -47,7 +47,7 @@
<string name="main_jump_title">페이지 이동</string> <string name="main_jump_title">페이지 이동</string>
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string> <string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string> <string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
<string name="main_move">%1$d 페이지로 이동</string> <string name="main_move_to_page">%1$d 페이지로 이동</string>
<string name="settings_clear_downloads">다운로드 삭제</string> <string name="settings_clear_downloads">다운로드 삭제</string>
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string> <string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
<string name="main_drawer_favorite">즐겨찾기</string> <string name="main_drawer_favorite">즐겨찾기</string>
@@ -71,7 +71,7 @@
<string name="main_menu_sort">정렬</string> <string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string> <string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string> <string name="main_menu_sort_newest">시간순</string>
<string name="ignore_update">무시</string> <string name="ignore">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string> <string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="settings_dark_mode_title">다크 모드</string> <string name="settings_dark_mode_title">다크 모드</string>
<string name="settings_dark_mode_summary">딥 다크한 모오드</string> <string name="settings_dark_mode_summary">딥 다크한 모오드</string>
@@ -128,7 +128,7 @@
<string name="settings_lock_fingerprint_prompt_subtitle">힘세고 강한 지문 인식</string> <string name="settings_lock_fingerprint_prompt_subtitle">힘세고 강한 지문 인식</string>
<string name="default_query_dialog_filter_loli">판사님 저는 페도가 아닙니다</string> <string name="default_query_dialog_filter_loli">판사님 저는 페도가 아닙니다</string>
<string name="settings_user_id">유저 ID</string> <string name="settings_user_id">유저 ID</string>
<string name="settings_user_id_toast">유저 ID를 클립보드에 복사했습니다</string> <string name="copied_to_clipboard">클립보드에 복사</string>
<string name="reader_fab_retry">재시도</string> <string name="reader_fab_retry">재시도</string>
<string name="reader_fab_auto">눈 깜빡임 감지 스크롤</string> <string name="reader_fab_auto">눈 깜빡임 감지 스크롤</string>
<string name="search_all">모든 갤러리 검색</string> <string name="search_all">모든 갤러리 검색</string>
@@ -151,5 +151,11 @@
<string name="no_camera">이 장치에는 전면 카메라가 없습니다</string> <string name="no_camera">이 장치에는 전면 카메라가 없습니다</string>
<string name="error">오류</string> <string name="error">오류</string>
<string name="settings_cache_limit">캐시 크기 제한</string> <string name="settings_cache_limit">캐시 크기 제한</string>
<string name="settings_cache_unlimited">무제한</string> <string name="unlimited">무제한</string>
<string name="settings_tag_translation">태그 언어</string>
<string name="settings_tag_translation_message">Github에서 번역에 참여하세요</string>
<string name="settings_max_concurrent_download">병렬 다운로드</string>
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
<string name="settings_networking">네트워크</string>
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
</resources> </resources>

View File

@@ -48,11 +48,6 @@
<item>japanese|日本語</item> <item>japanese|日本語</item>
</string-array> </string-array>
<string-array name="mirrors">
<item>HITOMI|hitomi.la</item>
<item>HIYOBI|hiyobi.me</item>
</string-array>
<string-array name="proxy_type"> <string-array name="proxy_type">
<item>Direct</item> <item>Direct</item>
<item>HTTP</item> <item>HTTP</item>
@@ -70,7 +65,7 @@
</string-array> </string-array>
<string-array name="cache_size_text"> <string-array name="cache_size_text">
<item>@string/settings_cache_unlimited</item> <item>@string/unlimited</item>
<item>1G</item> <item>1G</item>
<item>2G</item> <item>2G</item>
<item>4G</item> <item>4G</item>
@@ -79,4 +74,24 @@
<item>32G</item> <item>32G</item>
</string-array> </string-array>
<string-array name="concurrent_download">
<item>0</item>
<item>1</item>
<item>2</item>
<item>4</item>
<item>8</item>
<item>16</item>
<item>32</item>
</string-array>
<string-array name="concurrent_download_text">
<item>@string/unlimited</item>
<item>1</item>
<item>2</item>
<item>4</item>
<item>8</item>
<item>16</item>
<item>32</item>
</string-array>
</resources> </resources>

View File

@@ -21,4 +21,11 @@
<declare-styleable name="TagChipGroup"> <declare-styleable name="TagChipGroup">
<attr name="maxTag" format="integer"/> <attr name="maxTag" format="integer"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="RippleCircleStatus">
<attr name="half" format="enum">
<enum name="top" value="1"/>
<enum name="bottom" value="-1"/>
</attr>
</declare-styleable>
</resources> </resources>

View File

@@ -4,6 +4,8 @@
<color name="colorPrimaryDark">#0093c4</color> <color name="colorPrimaryDark">#0093c4</color>
<color name="colorAccent">#D81B60</color> <color name="colorAccent">#D81B60</color>
<color name="material_light_blue_300">#4fc3f7</color>
<color name="material_light_blue_700">#0288d1</color>
<color name="material_pink_600">#d81b60</color> <color name="material_pink_600">#d81b60</color>
<color name="material_blue_700">#1976d2</color> <color name="material_blue_700">#1976d2</color>
<color name="material_green_a700">#00c853</color> <color name="material_green_a700">#00c853</color>

View File

@@ -28,7 +28,11 @@
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="error">Error</string> <string name="error">Error</string>
<string name="ignore_update">Ignore</string> <string name="ignore">Ignore</string>
<string name="unlimited">Unlimited</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="channel_download">Download</string> <string name="channel_download">Download</string>
<string name="channel_download_description">Shows download status</string> <string name="channel_download_description">Shows download status</string>
@@ -45,6 +49,8 @@
<string name="main_no_result">No result</string> <string name="main_no_result">No result</string>
<string name="unaccessible_download_folder">From Android 11 and above, current Download folder cannot be accessed by outside apps. Would you like to change the download folder?</string>
<string name="main_drawer_home">Home</string> <string name="main_drawer_home">Home</string>
<string name="main_drawer_history">History</string> <string name="main_drawer_history">History</string>
<string name="main_drawer_downloads">Downloads</string> <string name="main_drawer_downloads">Downloads</string>
@@ -69,7 +75,7 @@
<string name="main_fab_random">Open a random gallery</string> <string name="main_fab_random">Open a random gallery</string>
<string name="main_fab_cancel">Cancel all downloads</string> <string name="main_fab_cancel">Cancel all downloads</string>
<string name="main_move">Move to page %1$d</string> <string name="main_move_to_page">Move to page %1$d</string>
<string name="main_download">DOWNLOAD</string> <string name="main_download">DOWNLOAD</string>
<string name="main_delete">DELETE</string> <string name="main_delete">DELETE</string>
@@ -144,6 +150,7 @@
<string name="settings_storage_usage_loading">Calculating storage usage…</string> <string name="settings_storage_usage_loading">Calculating storage usage…</string>
<string name="settings_clear_cache">Clear cache</string> <string name="settings_clear_cache">Clear cache</string>
<string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string> <string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string>
<string name="settings_recover_downloads">Reconstruct download database</string>
<string name="settings_clear_downloads">Clear downloads</string> <string name="settings_clear_downloads">Clear downloads</string>
<string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string> <string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string>
<string name="settings_clear_history">Clear history</string> <string name="settings_clear_history">Clear history</string>
@@ -162,7 +169,6 @@
<string name="settings_download_folder_custom">Custom Location</string> <string name="settings_download_folder_custom">Custom Location</string>
<string name="settings_download_folder_not_writable">This folder is not writable. Please select another folder.</string> <string name="settings_download_folder_not_writable">This folder is not writable. Please select another folder.</string>
<string name="settings_cache_limit">Cache Limit</string> <string name="settings_cache_limit">Cache Limit</string>
<string name="settings_cache_unlimited">Unlimited</string>
<string name="settings_nomedia_title">Hide image from gallery</string> <string name="settings_nomedia_title">Hide image from gallery</string>
<string name="settings_low_quality">Low quality images</string> <string name="settings_low_quality">Low quality images</string>
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string> <string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
@@ -172,11 +178,17 @@
<string name="settings_app_lock">App lock</string> <string name="settings_app_lock">App lock</string>
<string name="settings_app_lock_type">App lock type</string> <string name="settings_app_lock_type">App lock type</string>
<!-- SETTINGS/NETWORKING -->
<string name="settings_networking">Networking</string>
<string name="settings_mirror_summary">Load images from mirrors</string>
<string name="settings_proxy_title">Proxy</string>
<string name="settings_max_concurrent_download">Concurrent Download</string>
<!-- SETTINGS/MISCELLANEOUS --> <!-- SETTINGS/MISCELLANEOUS -->
<string name="settings_miscellaneous_title">Miscellaneous</string> <string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_mirror_summary">Load images from mirrors</string> <string name="settings_tag_translation">Tag Language</string>
<string name="settings_proxy_title">Proxy</string> <string name="settings_tag_translation_message">Participate in translation on Github</string>
<string name="settings_rtl">Turn pages Right-to-Left</string> <string name="settings_rtl">Turn pages Right-to-Left</string>
<string name="settings_security_mode_title">Enable security mode</string> <string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string> <string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
@@ -184,7 +196,6 @@
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string> <string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
<string name="settings_import_old_galleries">Import old galleries</string> <string name="settings_import_old_galleries">Import old galleries</string>
<string name="settings_user_id">User ID</string> <string name="settings_user_id">User ID</string>
<string name="settings_user_id_toast">User ID is copied to clipboard</string>
<string name="settings_oss">Open Source Notice</string> <string name="settings_oss">Open Source Notice</string>
<!-- MANAGE FAVORITES --> <!-- MANAGE FAVORITES -->

View File

@@ -27,6 +27,11 @@
app:key="delete_downloads" app:key="delete_downloads"
app:title="@string/settings_clear_downloads"/> app:title="@string/settings_clear_downloads"/>
<Preference
app:key="recover_downloads"
app:title="@string/settings_recover_downloads"
app:iconSpaceReserved="true"/>
<Preference <Preference
app:key="clear_history" app:key="clear_history"
app:title="@string/settings_clear_history"/> app:title="@string/settings_clear_history"/>

View File

@@ -18,6 +18,9 @@
--> -->
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
</domain-config>
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">ix.io</domain> <domain includeSubdomains="false">ix.io</domain>
</domain-config> </domain-config>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:android="http://schemas.android.com/apk/res/android">
<Preference <Preference
app:key="app_version" app:key="app_version"
@@ -56,12 +56,6 @@
app:key="nomedia" app:key="nomedia"
app:title="@string/settings_nomedia_title"/> app:title="@string/settings_nomedia_title"/>
<SwitchPreferenceCompat
app:key="low_quality"
app:title="@string/settings_low_quality"
app:summary="@string/settings_low_quality_summary"
app:defaultValue="true"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
@@ -74,17 +68,30 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
app:title="@string/settings_miscellaneous_title"> app:title="@string/settings_networking">
<Preference
app:key="mirrors"
app:title="@string/settings_mirror_title"
app:summary="@string/settings_mirror_summary"/>
<Preference <Preference
app:key="proxy" app:key="proxy"
app:title="@string/settings_proxy_title"/> app:title="@string/settings_proxy_title"/>
<ListPreference
app:key="max_concurrent_download"
android:title="@string/settings_max_concurrent_download"
app:entries="@array/concurrent_download_text"
app:entryValues="@array/concurrent_download"
android:defaultValue="0"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/settings_miscellaneous_title">
<ListPreference
app:key="tag_translation"
app:title="@string/settings_tag_translation"
app:useSimpleSummaryProvider="true"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="rtl" app:key="rtl"
app:title="@string/settings_rtl" app:title="@string/settings_rtl"

View File

@@ -26,21 +26,14 @@ package xyz.quaver.pupil
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
val a = mutableSetOf<Int>() val a = mutableSetOf<Int>()
print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType) print(a::class.java.methods.firstOrNull { it.name == "add" }?.genericParameterTypes?.firstOrNull() as? ParameterizedType)
} }
} }

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