Compare commits

...
278 Commits
Author SHA1 Message Date
tarsin 238d769f27 Release 1.8.3.0
Releases / Build (push) Has been cancelled
Just stop spamming for something like "I can install", If U still can't install then please SMASH YOUR DEVICE OR REPLACE YOUR GODDAMN SYSTEM
2022-11-21 16:48:50 +08:00
tarsin 1e648cc6b4 Nullability checks
extractToByteBuffer may return null object for some reason
2022-11-21 12:15:23 +08:00
tarsin 7ac20d59fc Use DirectByteBuffer
Seems my mimalloc(I use it as Android's system allocator) doesn't like release memory to system since I replace MADV_DONTNEED with MADV_FREE
https://github.com/AcmeUI/android_external_mimalloc
but there is no memleak I sure
2022-11-21 12:11:35 +08:00
tarsin 1d8ab51a22 Res cleanup 2022-11-20 21:06:11 +08:00
tarsin cdfb3b74b3 Code cleanup 2022-11-20 20:57:16 +08:00
小白-白 6f2868f7c3 Support select all on fav page (#437) 2022-11-20 19:18:43 +08:00
tarsin f3061fa497 Kotlin 1.8 and Java 19 2022-11-20 19:09:27 +08:00
FooIbar 468cce5f3d Remove fix thumb url (#431) 2022-11-20 10:03:21 +08:00
小白-白 bfaba80d3b Fix backup online fav list to local 2022-11-20 09:09:36 +08:00
小白-白 2ca4a2af86 Restrict datepicker ranges with validators 2022-11-20 09:09:36 +08:00
tarsin 7d0b245d3d Unroll loops 2022-11-20 00:09:00 +08:00
tarsin 96fcf69bdd Guard blur method 2022-11-19 23:01:44 +08:00
tarsin 63d95b1265 Guard some methods 2022-11-19 22:57:34 +08:00
tarsin d173e83e01 Resolve #421 2022-11-19 22:24:38 +08:00
tarsin 54d11dea95 Initial migrate to ImageDecoder Java api, and downgrade minsdk to 28 2022-11-19 22:17:08 +08:00
tarsin fbdab389c4 Let's target JVM 18 2022-11-17 23:39:06 +08:00
tarsin 3dd3bad362 Resolve #393 2022-11-17 21:54:05 +08:00
tarsin 65a9a4b4f2 Make Settings appbar liftonscroll 2022-11-17 21:34:28 +08:00
tarsin 5f6cf5550d Rename .java to .kt 2022-11-17 21:34:28 +08:00
tarsin 673dad4032 Fix comment send ui color 2022-11-17 19:33:12 +08:00
tarsin 5e45ec9a9c Restrict datepicker ranges 2022-11-17 18:11:02 +08:00
tarsin 306eb9d23c Bump version
Releases / Build (push) Has been cancelled
2022-11-17 12:36:19 +08:00
tarsin 158218e501 No jump menu when cannot jump pages 2022-11-17 12:22:19 +08:00
tarsin d773332b74 Use stable version of mdc 2022-11-17 12:21:59 +08:00
tarsin 5dc2eb0062 Expand title on CollapsingToolbarLayout 2022-11-17 11:31:40 +08:00
tarsin bee7febafc Use compressed toolbar 2022-11-17 00:45:38 +08:00
tarsin f49cc2d498 Fix GalleryCommentsScene fab and edittext render out of screen 2022-11-17 00:34:10 +08:00
tarsin 3f4d2b75fc Correctly use attach behaviour, not padding 2022-11-16 23:48:20 +08:00
tarsin af8733c2d1 Explicitly set LiftOnScrollTargetView 2022-11-16 22:46:24 +08:00
tarsin 545eed98a7 Support lift Appbar when scroll, use CollapsingAppbar on gallery detail for long title 2022-11-16 21:50:31 +08:00
tarsin 2da436189a Allow overscroll on GalleryDetailScene, also set title on appbar, Resolve #277 2022-11-16 17:26:27 +08:00
tarsin 906ea11733 Rename .java to .kt 2022-11-16 17:26:27 +08:00
小白-白 47c5629f7d Fix fav page load after jump 2022-11-16 11:52:41 +08:00
tarsin a37c676d96 Update README.md 2022-11-15 22:36:16 +08:00
tarsin d9981f15f2 Bump version
Releases / Build (push) Has been cancelled
2022-11-15 22:19:21 +08:00
tarsin 27b75746fd Simplify clipboardutils 2022-11-15 22:17:12 +08:00
tarsin ee80a3a841 Copy to Clipboard refactor 2022-11-15 22:05:26 +08:00
tarsin f8d639054f Just use normal rv 2022-11-15 21:48:22 +08:00
tarsin 7f76e58e4e Move GalleryInfoBottomSheet out of scene package to allow obfuscate and repackage 2022-11-15 21:46:00 +08:00
tarsin 1b326a86a2 Convert GalleryInfoScene to BottomSheet, Resolve #354 2022-11-15 21:38:27 +08:00
tarsin 607bf4c67f Use inverse color on snackbar, #326 2022-11-15 15:30:44 +08:00
小白-白 3e4836c91e Support favorites prev page load 2022-11-15 12:54:41 +08:00
小白-白 dae8f21388 Support prev page load 2022-11-15 12:54:41 +08:00
小白-白 84b328b60b Init prev page load 2022-11-15 12:54:41 +08:00
小白-白 b98d55b444 Hide jump button on local fav cat 2022-11-15 12:54:41 +08:00
小白-白 ee9413e54a Fix favorite page jump 2022-11-15 12:54:41 +08:00
小白-白 76dedce5da Remove unavailable search options 2022-11-15 12:54:41 +08:00
tarsin 37f99ae706 Fuck off the "Press twice to exit"
It can't be more stupid on our modern gesture navigation
2022-11-14 19:34:37 +08:00
tarsin aee58d967d Kotlin 1.7.21 2022-11-14 14:42:41 +08:00
tarsin 5ecabeb045 Rearrange gallery actions, resolve #413 2022-11-14 14:29:02 +08:00
tarsin 408c142aad Haptic feedback for slider, #408 2022-11-14 14:09:54 +08:00
tarsin a56d541380 fixup! Adjust madvise usage 2022-11-10 15:28:06 +08:00
tarsin ae79417845 Adjust madvise usage 2022-11-10 15:21:17 +08:00
小白-白 010daa583e Remove unavailable advanced search options 2022-11-10 12:36:08 +08:00
renovate[bot] 3034957c99 Update room_version to v2.5.0-beta02 (1.8.x.x) (#402)
Update room_version to v2.5.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-10 08:23:57 +08:00
renovate[bot] 99134346ed Update dependency androidx.compose.material3:material3 to v1.0.1 (1.8.x.x) (#399)
Update dependency androidx.compose.material3:material3 to v1.0.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-10 08:23:00 +08:00
renovate[bot] 9a70708300 Update compose_version to v1.3.1 2022-11-09 23:13:03 +00:00
tarsin 50f0df5012 Explicitly set format and filter we use
This helps to reduce native library size
2022-11-10 00:56:49 +08:00
小白-白 599bb80106 Fix toplist page load 2022-11-09 23:15:38 +08:00
小白-白 aa0db1f400 Hide jump button on the popular page 2022-11-09 23:15:38 +08:00
skymkmk 82b6fd22a9 Enhancement: Add delete image file option to the dialog on the gallery page (#391) 2022-11-08 15:26:29 +08:00
renovate[bot] b5c730d37a Update plugin com.android.application to v8.0.0-alpha08 (1.8.x.x) (#388)
Update plugin com.android.application to v8.0.0-alpha08

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 04:36:48 +00:00
txyyh a2cb6444f4 Revert "Remove preview count limit #332 (#333)"
This reverts commit 783fa10157.
2022-11-07 10:28:34 +00:00
tarsin 8a144eabca Reset ContentLayout changes
This component is quite a shit, further we must rewrite it to better adapt new page-off gid-based load design
2022-11-07 18:26:27 +08:00
tarsin 75c2a3b0ec Tweak HistoryScene with modern kotlin 2022-11-06 23:19:15 +08:00
tarsin beec02a69b Fix #370 2022-11-06 21:59:08 +08:00
tarsin df78b45f69 Initial support for jump to certain date 2022-11-06 14:58:05 +08:00
tarsin e6a3c8f165 Rename .java to .kt 2022-11-06 14:58:05 +08:00
tarsin 4d948e93a3 Datepicker stub 2022-11-06 13:23:49 +08:00
tarsin ece082727b Release 1.8.3.0-alpha04
Releases / Build (push) Has been cancelled
2022-11-06 13:02:55 +08:00
tarsin 5b5040f4af No gallery found no more a exception 2022-11-06 13:02:05 +08:00
tarsin d9d88e690b Fix favourite list load first 2022-11-06 12:57:51 +08:00
tarsin bda123fc39 Build with JDK 19
Gradle 8.0.0-M3 should be able to work with JDK19
2022-11-06 00:22:59 +08:00
tarsin 2200bb9b45 Upgrade java compatibility to Java 17 2022-11-06 00:21:25 +08:00
txyyh 884d460b52 Rel 1.8.3.0-alpha03
Releases / Build (push) Has been cancelled
2022-11-05 09:50:13 +00:00
tarsin beb97270a0 Initial support for predictive back gesture
https://developer.android.com/guide/navigation/predictive-back-gesture
2022-11-05 12:48:03 +08:00
小白-白 52681bd386 Fix favorite page load (#368)
修复收藏页面按发布时间排序时无法翻页问题
2022-11-04 11:40:30 +08:00
renovate[bot] d772a737fc Update plugin com.google.devtools.ksp to v1.7.20-1.0.8 (1.8.x.x) (#367)
Update plugin com.google.devtools.ksp to v1.7.20-1.0.8

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-04 11:15:20 +08:00
tarsin 638fcfea12 Do not cancel a sent call
Should fix slow load speed
2022-11-04 00:22:25 +08:00
tarsin cae32cca5a Rel 1.8.3.0-alpha02
Releases / Build (push) Has been cancelled
2022-11-03 23:19:06 +08:00
tarsin c429e7054a Don't parse found in common path 2022-11-03 23:11:33 +08:00
tarsin f526f11c73 Fix tag search page load 2022-11-03 22:59:27 +08:00
tarsin 892016368f Fix favourite scene data restore 2022-11-03 22:54:28 +08:00
tarsin d9464ac278 Fix uploader page request 2022-11-03 16:20:47 +08:00
tarsin a68a9995a1 Rel 1.8.3.0-alpha01
Releases / Build (push) Has been cancelled
2022-11-03 16:07:16 +08:00
tarsin 1a0c2cb180 Relax founds limit
We will calculate accurately later
2022-11-03 16:01:50 +08:00
tarsin d7572f5a79 Fix restore state 2022-11-03 15:48:35 +08:00
tarsin a208651e13 Code clean and allow refresh pages 2022-11-03 15:31:31 +08:00
tarsin 41c231b4f3 Bump version to 1.8.3.0-alpha
Releases / Build (push) Has been cancelled
2022-11-03 14:13:32 +08:00
tarsin 567b6f7659 Minimal changes to rework Favourite grabs
TODO: Reimpl goto, refresh pages
And take care of per page counts by user
2022-11-03 13:18:38 +08:00
tarsin 6d1e0089e5 Minimal changes to rework new exhentai page load
TODO: Reimpl Favourite grab, goto, refresh pages
2022-11-03 11:48:04 +08:00
renovate[bot] 502f532fe7 Update plugin com.android.application to v8.0.0-alpha07 (1.8.x.x) (#357)
Update plugin com.android.application to v8.0.0-alpha07

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 19:24:59 +00:00
tarsin ddc47f09ab Resolve #348 2022-11-01 11:18:44 +08:00
tarsin 5dbcdf2fce Use thinlto 2022-11-01 00:00:07 +08:00
tarsin 706588c39c Kotlinize 2022-10-31 23:53:44 +08:00
tarsin 3e3e64942e Rename .java to .kt 2022-10-31 23:53:44 +08:00
renovate[bot] 3700ac8a84 Update dependency gradle to v8.0-milestone-3 (1.8.x.x) (#350)
Update dependency gradle to v8.0-milestone-3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-28 23:05:06 +08:00
renovate[bot] cc0dc0abf5 Update lifecycle_version to v2.6.0-alpha03 (1.8.x.x) (#340)
Update lifecycle_version to v2.6.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:55 +08:00
renovate[bot] 5861147d17 Update dependency androidx.paging:paging-runtime to v3.2.0-alpha03 (1.8.x.x) (#338)
Update dependency androidx.paging:paging-runtime to v3.2.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:42 +08:00
renovate[bot] 732035a0e2 Update dependency com.google.android.material:material to v1.8.0-alpha02 (1.8.x.x) (#339)
Update dependency com.google.android.material:material to v1.8.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:28 +08:00
renovate[bot] 643f71009d Update nav_version to v2.5.3 (1.8.x.x) (#341)
Update nav_version to v2.5.3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:12 +08:00
renovate[bot] 4643e37adb Update plugin com.android.application to v8.0.0-alpha06 (1.8.x.x) (#342)
Update plugin com.android.application to v8.0.0-alpha06

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:13:55 +08:00
renovate[bot] 1bc8854685 Update dependency androidx.compose.material3:material3 to v1.0.0 (1.8.x.x) (#337)
Update dependency androidx.compose.material3:material3 to v1.0.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 02:07:28 +00:00
renovate[bot] 66ebf6a391 Update compose_version to v1.3.0 (1.8.x.x) (#336)
Update compose_version to v1.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 23:01:21 +00:00
renovate[bot] 1239277cec Update activity_version to v1.6.1 (1.8.x.x) (#335)
Update activity_version to v1.6.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 19:34:27 +00:00
tarsin 9cb8fb5527 Enable new resources shrinker
And full mode r8 is enabled by default on AGP8 above
2022-10-23 19:04:56 +08:00
tarsin 819d67dca8 Upgrade to gradle 8.0.0M2 2022-10-23 13:05:29 +08:00
tarsin f895982fa0 Revert to jdk 18 2022-10-23 12:45:59 +08:00
tarsin 505c097659 Build with jdk19 and enable parallel-gc
https://blog.bitrise.io/post/switch-to-jdk-17-parallel-gc-and-speed-up-your-android-builds-by-9-20
2022-10-23 12:35:05 +08:00
小白-白 783fa10157 Remove preview count limit #332 (#333)
fix: preview count limit
2022-10-23 01:25:06 +08:00
tarsin 82ffbf2a1d Reset unlock status if within delay time 2022-10-20 13:33:50 +08:00
renovate[bot] 3c3ae47511 Update plugin com.android.application to v8.0.0-alpha05 (1.8.x.x) (#331)
Update plugin com.android.application to v8.0.0-alpha05

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-18 19:44:59 +00:00
tarsin 858d7acf68 Fix code style 2022-10-17 00:43:35 +08:00
renovate[bot] f8b7005690 Update plugin com.android.application to v8.0.0-alpha04 (1.8.x.x) (#330)
Update plugin com.android.application to v8.0.0-alpha04

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-15 03:29:10 +00:00
renovate[bot] 33a8cb98e6 Update plugin com.google.devtools.ksp to v1.7.20-1.0.7 (1.8.x.x) (#329)
Update plugin com.google.devtools.ksp to v1.7.20-1.0.7

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-14 02:17:50 +00:00
tarsin 338a22f2ed No need to trigger gc manually 2022-10-09 14:44:42 +08:00
tarsin f7cecaf709 on stack tile buffer 2022-10-08 21:05:42 +08:00
tarsin c38b3c15e7 Tweak tags color 2022-10-08 20:48:40 +08:00
tarsin 4665678e9f fixup! Use MaterialIconButton 2022-10-08 20:35:43 +08:00
tarsin 07416eb74d Use MaterialIconButton 2022-10-07 19:20:01 +08:00
renovate[bot] 6f37dc53c7 Update plugin com.android.application to v8.0.0-alpha03 (1.8.x.x) (#327)
Update plugin com.android.application to v8.0.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-07 02:07:07 +00:00
tarsin 55b963dcf9 Kotlin 1.7.20 2022-10-06 12:29:35 +08:00
txyyh 1a433019b3 Add strikethrough (#325) 2022-10-06 12:21:02 +08:00
renovate[bot] 7b292cb1ab Update dependency androidx.compose.material3:material3 to v1.0.0-rc01 (1.8.x.x) (#322)
Update dependency androidx.compose.material3:material3 to v1.0.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 11:07:03 +08:00
renovate[bot] b2f5590c04 Update fragment_version to v1.6.0-alpha03 (1.8.x.x) (#323)
Update fragment_version to v1.6.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 09:36:07 +08:00
renovate[bot] 2cf2c87a8b Update room_version to v2.5.0-beta01 (1.8.x.x) (#324)
Update room_version to v2.5.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 09:35:54 +08:00
renovate[bot] 01420f7e72 Update compose_version to v1.3.0-rc01 (1.8.x.x) (#321)
Update compose_version to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-05 23:01:01 +00:00
txyyh 3f699973b9 Release 1.8.2.4
Releases / Build (push) Has been cancelled
2022-10-05 14:25:33 +00:00
renovate[bot] 3f27c9139c Update plugin com.android.application to v8.0.0-alpha02 (1.8.x.x) (#313)
Update plugin com.android.application to v8.0.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-27 22:42:23 +00:00
tarsin 9eee6fc4af Clipboard management refactor, kill all tips when copy to clipboard on Tiramisu above 2022-09-27 00:45:23 +08:00
tarsin a343dbe8f8 Rename .java to .kt 2022-09-27 00:45:23 +08:00
tarsin bc08a09621 Fix read archive 2022-09-23 09:20:15 +08:00
tarsin 586ad66bff Upgrade to AGP 8
And avoid annoying cmake warnings as well
2022-09-22 20:29:53 +08:00
tarsin 1a426e1f5f Simplify code 2022-09-22 20:29:18 +08:00
tarsin e9a9f1070c Kotlinize 2022-09-22 20:29:18 +08:00
tarsin 7d3dd61bc4 Rename .java to .kt 2022-09-22 20:29:18 +08:00
老虎豆 f37894af88 Fix when clicking cover in download that don't enter gallery detail(#251) (#305)
Fix when clicking cover in download that don't enter gallery detail
2022-09-22 19:41:56 +08:00
renovate[bot] 14129545b8 Update dependency androidx.recyclerview:recyclerview to v1.3.0-rc01 (1.8.x.x) (#297)
Update dependency androidx.recyclerview:recyclerview to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 10:30:25 +08:00
renovate[bot] f06346b6c7 Update dependency androidx.customview:customview to v1.2.0-alpha02 (1.8.x.x) (#296)
Update dependency androidx.customview:customview to v1.2.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 09:51:53 +08:00
renovate[bot] 13e3e8c534 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha04 (1.8.x.x) (#295)
Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha04

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 08:14:13 +08:00
renovate[bot] 4be282813c Update dependency androidx.compose.material3:material3 to v1.0.0-beta03 (1.8.x.x) (#294)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 08:14:01 +08:00
renovate[bot] 74e66a407d Update compose_version to v1.3.0-beta03 (1.8.x.x) (#291)
Update compose_version to v1.3.0-beta03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:55:08 +08:00
renovate[bot] b19da4c82e Update dependency androidx.annotation:annotation to v1.5.0 (1.8.x.x) (#292)
Update dependency androidx.annotation:annotation to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:54:41 +08:00
renovate[bot] 2f49764e1b Update dependency androidx.biometric:biometric-ktx to v1.2.0-alpha05 (1.8.x.x) (#293)
Update dependency androidx.biometric:biometric-ktx to v1.2.0-alpha05

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:54:23 +08:00
renovate[bot] 08dfb0f72d Update activity_version to v1.6.0 (1.8.x.x) (#290)
Update activity_version to v1.6.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 21:38:55 +00:00
txyyh 9251ed238c Fix archive code error (#289) 2022-09-21 08:56:56 +08:00
tarsin 3b96328765 Use fixed size buffer 2022-09-20 18:29:35 +08:00
tarsin 108928e524 Implement save, share image from archive 2022-09-20 18:09:41 +08:00
LaLucid b1191bbf9b Add Gradle cache clean (#287) 2022-09-18 18:00:24 +08:00
txyyh 3424260cfd Release 1.8.2.3
Releases / Build (push) Has been cancelled
2022-09-18 03:31:58 +00:00
EdwardWu e1fe56160e Ehviewer: Translate: Update zh-rTW Translate (09/17) (#284)
* Update string from beadc7 (Merge Account and Signout)
* Update string from 1a6e03 (Add Lock Delay)

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-18 01:26:10 +08:00
tarsin 5838ea3e3f Fix #282 2022-09-17 15:11:34 +08:00
txyyh 0906b17ed1 Use example version code in issue template 2022-09-17 13:44:56 +08:00
txyyh 49b9cb72d3 Apply format to all docs 2022-09-16 12:40:28 +00:00
tarsin 01508cc3c1 Update README.md 2022-09-16 13:17:45 +08:00
tarsin 79eba83812 fixup! Release 1.8.2.2
Releases / Build (push) Has been cancelled
2022-09-16 12:42:27 +08:00
tarsin a455dee24c Release 1.8.2.2 2022-09-16 12:41:39 +08:00
tarsin f58b4959e4 Reformat code 2022-09-16 12:39:50 +08:00
tarsin a6b4182ecd Don't require External storage [1.8.x.x] 2022-09-16 12:37:06 +08:00
tarsin 9010591ffc Import Download label first, Fix #245 2022-09-16 11:53:48 +08:00
tarsin e0dde2c982 Read database permissively, Fix #274 2022-09-16 11:40:02 +08:00
tarsin 6109d97bf7 Do not record last leave time if app not unlocked, And Fix #275 2022-09-16 11:14:11 +08:00
tarsin 95936e01ce Drop nativelib version dialog 2022-09-16 10:50:04 +08:00
tarsin beadc7b185 Merge Account and Signout 2022-09-16 10:46:47 +08:00
tarsin 0bc2e45137 Merge Account and Cookies 2022-09-16 10:35:40 +08:00
tarsin b8e649e995 Add support to visit login status 2022-09-16 10:30:16 +08:00
tarsin bdcd21a535 Rename .java to .kt 2022-09-16 10:30:15 +08:00
tarsin 1b779a8b77 Disable unlock delay seekbar as well if Authenticators not available 2022-09-16 09:52:16 +08:00
renovate[bot] 6c59a341ee Update plugin com.android.application to v7.3.0 (1.8.x.x) (#279)
Update plugin com.android.application to v7.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-15 22:37:00 +00:00
老虎豆 1a6e03c8f8 Add Lock Delay(#276) (#278)
* Add Lock Delay(#276)

* Remove zh-TW translation

* Use SeekBarPreference instead of SimpleMenuPreference
2022-09-16 02:24:49 +08:00
txyyh 653c0a2ac6 Add thanks to issue template (#272)
* Add thanks

* Shorten description
2022-09-12 18:51:43 +08:00
renovate[bot] e4189f367a Update nav_version to v2.5.2 (1.8.x.x) (#265)
Update nav_version to v2.5.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 20:11:10 +00:00
renovate[bot] 48b7355ba4 Update dependency androidx.compose.material3:material3 to v1.0.0-beta02 (1.8.x.x) (#263)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 15:35:49 +08:00
renovate[bot] d7d3a903ff Update lifecycle_version to v2.6.0-alpha02 (1.8.x.x) (#264)
Update lifecycle_version to v2.6.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 15:35:36 +08:00
renovate[bot] bab3067273 Update core_version to v1.9.0 (1.8.x.x) (#259)
Update core_version to v1.9.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:32:34 +08:00
renovate[bot] fdd70a6908 Update dependency androidx.annotation:annotation to v1.5.0-rc01 (1.8.x.x) (#260)
Update dependency androidx.annotation:annotation to v1.5.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:32:22 +08:00
renovate[bot] 3699a8fe30 Update dependency androidx.annotation:annotation-experimental to v1.3.0 (1.8.x.x) (#261)
Update dependency androidx.annotation:annotation-experimental to v1.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:30:05 +08:00
renovate[bot] 32f98f8a15 Update dependency androidx.appcompat:appcompat to v1.6.0-rc01 (1.8.x.x) (#262)
Update dependency androidx.appcompat:appcompat to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:27:18 +08:00
renovate[bot] c5366760a2 Update compose_version to v1.3.0-beta02 (1.8.x.x) (#258)
Update compose_version to v1.3.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 02:03:25 +00:00
renovate[bot] c98ec2a7a4 Update activity_version to v1.6.0-rc02 (1.8.x.x) (#257)
Update activity_version to v1.6.0-rc02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-07 21:47:13 +00:00
txyyh e4c729e51b Add additional context textarea (#255)
* Use other word

* Add additional context to bug report
2022-09-07 08:31:10 +00:00
EdwardWu f36726d8fb Ehviewer: Translate: Update zh-rTW Translate (09/06) (#254)
* Update string from d2fc2e
* Update some strings translate

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-06 13:44:49 +08:00
LittleLoli d2fc2ee136 add backup remote favorite list to local. (#252) 2022-09-06 01:21:53 +08:00
EdwardWu 6edc2164af Ehviewer: Translate: Update zh-rTW Translate (09/04) (#250)
* Update string from a5161bb

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-04 23:55:22 +08:00
txyyh e34dc30f1f Update material-components (#249) 2022-09-03 08:21:16 +08:00
tarsin c03081d3fa Fix #246 2022-08-31 08:48:36 +08:00
renovate[bot] 97287e4538 Update dependency com.google.android.material:material to v1.7.0-rc01 (1.8.x.x) (#247)
Update dependency com.google.android.material:material to v1.7.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-31 08:43:42 +08:00
txyyh fc44507a34 Use label on info card (#243) 2022-08-29 16:24:26 +08:00
txyyh 2597004750 Use LabelLarge on CardMessage (#244) 2022-08-29 16:24:11 +08:00
txyyh d1bd9c5fae Use title small on comment (#242) 2022-08-29 16:23:32 +08:00
renovate[bot] 54542f9246 Update room_version to v2.5.0-alpha03 (1.8.x.x) (#236)
Update room_version to v2.5.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 14:40:38 +08:00
renovate[bot] 377b23458f Update fragment_version to v1.6.0-alpha02 (1.8.x.x) (#235)
Update fragment_version to v1.6.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 14:40:13 +08:00
renovate[bot] b7973cdb76 Update dependency androidx.webkit:webkit to v1.5.0 (1.8.x.x) (#234)
Update dependency androidx.webkit:webkit to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 06:24:40 +00:00
renovate[bot] 5f1a70886c Update dependency androidx.annotation:annotation-experimental to v1.3.0-rc01 (1.8.x.x) (#232)
Update dependency androidx.annotation:annotation-experimental to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:49:36 +08:00
renovate[bot] 5fbc8b0b66 Update dependency androidx.compose.material3:material3 to v1.0.0-beta01 (1.8.x.x) (#233)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:49:24 +08:00
renovate[bot] 780b2b6e1d Update compose_version to v1.3.0-beta01 (1.8.x.x) (#229)
Update compose_version to v1.3.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:45:25 +08:00
renovate[bot] 039ad2032c Update core_version to v1.9.0-rc01 (1.8.x.x) (#230)
Update core_version to v1.9.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:45:05 +08:00
renovate[bot] a24222b693 Update dependency androidx.annotation:annotation to v1.5.0-beta01 (1.8.x.x) (#231)
Update dependency androidx.annotation:annotation to v1.5.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:44:53 +08:00
renovate[bot] 6047d91d7e Update activity_version to v1.6.0-rc01 (1.8.x.x) (#228)
Update activity_version to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 22:18:38 +00:00
tarsin f0158f6946 Make dialog takes some blur 2022-08-24 21:00:41 +08:00
tarsin 00ecdc58e6 Upgrade ndk to 25.1.8937393 2022-08-24 18:04:55 +08:00
renovate[bot] 0a075a97af Update dependency org.jsoup:jsoup to v1.15.3 (1.8.x.x) (#225)
Update dependency org.jsoup:jsoup to v1.15.3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 05:56:11 +00:00
tarsin 34ee70ca7d Make download card takes proper round corner 2022-08-23 19:38:53 +08:00
tarsin 338751ef2b Drop unused code 2022-08-23 19:34:04 +08:00
tarsin d4465ba864 Drop unused color res
* Color should be dynamic most time
2022-08-23 19:11:23 +08:00
tarsin 9a8fdd4edb Make swiperefreshlayout and ProgressIndicator follow dyncolor 2022-08-23 19:05:10 +08:00
txyyh 338848f1eb Make release
Releases / Build (push) Has been cancelled
2022-08-22 11:01:25 +00:00
tarsin 9b918ec036 Revert "Reformat Code"
This reverts commit e0f7dab34f.
2022-08-22 17:27:52 +08:00
txyyh 24439c7853 Update screenshots 2022-08-22 07:20:07 +00:00
txyyh a7f63a5b57 Make release
Releases / Build (push) Has been cancelled
2022-08-22 05:02:51 +00:00
tarsin 1e68782056 Use Tiramisu's imageselector 2022-08-22 12:24:12 +08:00
tarsin e0f7dab34f Reformat Code 2022-08-22 11:38:03 +08:00
tarsin 5513d47b6d Fix #215 2022-08-22 11:30:25 +08:00
tarsin e12abb66f7 Rename .java to .kt 2022-08-22 11:30:25 +08:00
tarsin 106be8c0ca Nuke in-app locale selector on Tiramisu above 2022-08-22 10:18:17 +08:00
txyyh 2e0d065e4d Implement #128 (#217)
* Implement #128

* Add LICENSE Header

* Fix zh_CN zh_TW
2022-08-22 10:06:10 +08:00
tarsin 073298cfee Implement #127 2022-08-22 08:49:57 +08:00
tarsin ebd0f4b4f8 Rename .java to .kt 2022-08-22 08:49:57 +08:00
tarsin 0ba71b060c Fix a race condition 2022-08-21 12:31:10 +08:00
tarsin 144e384441 Refine error handling 2022-08-21 12:00:29 +08:00
tarsin 9bb694171b Implement multithreading archive read 2022-08-21 09:21:53 +08:00
tarsin 647362cd82 Implement multiple archive context, Fix #213
* This is useful for multithreading decompress with lock usage
2022-08-20 23:38:13 +08:00
tarsin 56996558df Fix a use-after-free 2022-08-20 20:23:57 +08:00
txyyh 956b7c719e Make release
Releases / Build (push) Has been cancelled
2022-08-15 14:30:55 +00:00
txyyh 56e13559a0 Describe CI pages 2022-08-15 14:27:45 +00:00
txyyh 261ed86047 Rename apks on releases 2022-08-15 14:24:48 +00:00
tarsin d9b18358d2 Fix a wrong place res 2022-08-15 22:00:19 +08:00
tarsin a5161bba6f Support resize thumb, Implement #193 2022-08-15 21:52:49 +08:00
tarsin 62b00c758c IsLightStatusBar always follow night mode, Fix #199 2022-08-15 20:42:44 +08:00
tarsin 8663b4fa40 Rename .java to .kt 2022-08-15 20:42:44 +08:00
tarsin 6304c12bd0 Fix #196 2022-08-15 20:05:29 +08:00
EdwardWu efe2473ecf Ehviewer: Re-Add warning (#195) 2022-08-12 22:38:46 +08:00
tarsin 6ba494a4f1 Fixup! Re-add warning
Releases / Build (push) Has been cancelled
2022-08-12 20:43:24 +08:00
tarsin b81572ffff Re-add warning 2022-08-12 20:40:13 +08:00
tarsin d2ae1567be import jetpack compose 2022-08-12 19:01:59 +08:00
tarsin d00055a5f8 Add back doh 2022-08-12 18:09:59 +08:00
tarsin 896a2684d9 Make release 2022-08-12 17:28:13 +08:00
tarsin b98ecb17d3 Checkout to nettle 3.8.1 2022-08-12 17:24:31 +08:00
tarsin cd9af7feee Silence a warning 2022-08-12 15:29:39 +08:00
tarsin 1c38f77b5c Refactor history with room paging 2022-08-12 15:29:04 +08:00
tarsin c5977e358e Rename .java to .kt 2022-08-12 15:29:04 +08:00
tarsin 78e58e6894 Import android paging 2022-08-12 12:02:54 +08:00
tarsin 30433c5cbf Drop history limit 2022-08-12 12:00:50 +08:00
renovate[bot] fe8e3766c0 Update dependency com.google.android.material:material to v1.7.0-beta01 2022-08-11 22:02:01 +00:00
txyyh 90c95d9788 Enable renovate automerge 2022-08-11 09:57:41 +00:00
tarsin 3930a79a47 Update AndroidManifest.xml 2022-08-11 14:17:32 +08:00
renovate[bot] fc852780f8 Update activity_version to v1.6.0-beta01 (1.8.x.x) (#177)
Update activity_version to v1.6.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:58 +08:00
renovate[bot] 35315c38cd Update dependency androidx.annotation:annotation to v1.5.0-alpha02 (1.8.x.x) (#179)
Update dependency androidx.annotation:annotation to v1.5.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:41 +08:00
renovate[bot] 0b40f37418 Update dependency androidx.webkit:webkit to v1.5.0-rc01 (1.8.x.x) (#183)
Update dependency androidx.webkit:webkit to v1.5.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:07 +08:00
renovate[bot] 5a5f0421d8 Update dependency androidx.annotation:annotation-experimental to v1.3.0-beta01 (1.8.x.x) (#180)
Update dependency androidx.annotation:annotation-experimental to v1.3.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:10:38 +08:00
renovate[bot] 86c5c5bc59 Update dependency androidx.appcompat:appcompat to v1.6.0-beta01 (1.8.x.x) (#181)
Update dependency androidx.appcompat:appcompat to v1.6.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:08:04 +08:00
renovate[bot] 776a088de9 Update core_version to v1.9.0-beta01 (1.8.x.x) (#178)
Update core_version to v1.9.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:07:45 +08:00
renovate[bot] ba54655877 Update dependency androidx.recyclerview:recyclerview to v1.3.0-beta02 (1.8.x.x) (#182)
Update dependency androidx.recyclerview:recyclerview to v1.3.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:07:30 +08:00
txyyh 9f47ae438c Disable blank issue 2022-08-10 16:49:39 +00:00
txyyh 1717233f5a Add LICENSE header 2022-08-10 09:00:26 +00:00
txyyh cf71feb89d Use ic_launcher_foreground on scene_login (#174) 2022-08-10 16:23:09 +08:00
txyyh 21e393246a Use new icon (#173)
* Use new icon

* Add monochrome icon
2022-08-10 15:45:15 +08:00
txyyh c368f234f0 Fix wrong deploy username 2022-08-09 05:04:44 +00:00
tarsin c3d8610e2a Update AndroidManifest.xml 2022-08-09 08:43:24 +08:00
txyyh 1a10e330aa Only build CI on branches (#170) 2022-08-08 00:59:21 +08:00
txyyh d992897506 Auto deploy release to fdroid repo (#169) 2022-08-07 19:21:55 +08:00
tarsin a0fe6f0d7d Revert "archive: Use memfd instead"
* It sucks, seems simplify the conditions, but more syscalls are
  made(read/seek/write), more context switch, futhermore AImageDecoder itself does support
  read raw buffer, and we are not intend to support multi-process decoding

This reverts commit f4826ea727.
2022-08-07 19:14:25 +08:00
tarsin f4826ea727 archive: Use memfd instead 2022-08-07 17:13:48 +08:00
tarsin a17b254494 Call mmap/munmap in native code 2022-08-07 15:23:32 +08:00
tarsin 0190fc07bd Fix thumb card margin 2022-08-07 14:38:27 +08:00
tarsin c8dab6a88f Archive: ignore crc32 2022-08-07 11:43:27 +08:00
renovate[bot] fbafadfd98 Update dependency gradle to v7.5.1 (1.8.x.x) (#167)
Update dependency gradle to v7.5.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-06 07:42:25 +08:00
tarsin b5832fd706 Make release
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-08-05 22:13:38 +08:00
tarsin 93312a0714 Dependencies tweaks 2022-08-05 22:13:03 +08:00
tarsin 7e6e1f6f14 Make default label removable 2022-08-05 21:14:38 +08:00
tarsin 6cd498af6a Make downloadList qs drawer drag to rearrange position and swipe to remove 2022-08-05 20:27:43 +08:00
tarsin 2781aaca14 Make gallerylist qs drawer drag to rearrange position and swipe to remove 2022-08-05 20:03:51 +08:00
tarsin 91b9c9af40 Reimplement history swipe-to-remove 2022-08-05 19:44:12 +08:00
tarsin 5c0d10b41a Clean up readme 2022-08-05 18:14:15 +08:00
tarsin a3bdbd7a85 Drop android-advancedrecyclerview
* Actually this library is unmaintained for above a year, and drag feature we used is totally broken on Android 12 and above https://github.com/h6ah4i/android-advancedrecyclerview/issues/516 https://github.com/Ehviewer-Overhauled/Ehviewer/issues/54
2022-08-05 18:12:23 +08:00
tarsin f66151f880 Clean up proguards 2022-08-05 15:13:59 +08:00
txyyh 6c5f52995d Add mult-language README and markdown TOC (#165)
* Add mult-language README

* Add markdown TOC

* Add language selection to README

* Fix wrong branch name in README
2022-08-05 14:55:03 +08:00
tarsin d94b96a8eb Clean up 2022-08-04 13:05:47 +08:00
243 changed files with 6457 additions and 9160 deletions

No files matched your search

+26 -4
View File
@@ -1,8 +1,17 @@
name: Bug 反馈 / Bug report
description: 提交一个问题报告 / Create a bug report to help us improve
description: 提交一个问题报告 / Create a bug report
labels:
- "bug"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before create bug report
- type: textarea
id: reproduce
attributes:
@@ -44,12 +53,25 @@ body:
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 四位版本号 / 4 digits version code
placeholder: 1.6.9.13
validations:
required: true
@@ -57,7 +79,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
@@ -119,4 +141,4 @@ body:
required: true
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
required: true
required: true
+1
View File
@@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
+14 -5
View File
@@ -1,8 +1,17 @@
name: 功能请求 / Feature request
description: 提出一个功能建议 / Suggest an idea for this project
description: 提出一个功能建议 / Suggest an idea
labels:
- "enhancement"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交功能建议前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
@@ -33,7 +42,7 @@ body:
- type: textarea
id: more
attributes:
label: 其他细节 / Addition details
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
@@ -48,7 +57,7 @@ body:
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 四位版本号 / 4 digits version code
placeholder: 1.6.9.13
validations:
required: true
@@ -56,7 +65,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
@@ -82,4 +91,4 @@ body:
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
required: true
+10 -4
View File
@@ -1,6 +1,11 @@
name: CI
on: [push]
on:
push:
branches:
- '*'
tags-ignore:
- "*"
jobs:
linux:
@@ -12,19 +17,20 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-cleanup: true
arguments: assembleRelease
- name: Update ARM64
+3 -3
View File
@@ -12,15 +12,15 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
+36 -8
View File
@@ -15,29 +15,57 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- name: Rename Apks
run: |
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal-A12+.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86-A12+.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
- name: Releases
uses: softprops/action-gh-release@v1
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/app-universal-release.apk
app/build/outputs/apk/release/app-arm64-v8a-release.apk
app/build/outputs/apk/release/app-x86_64-release.apk
app/build/outputs/apk/release/app-x86-release.apk
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
app/build/outputs/apk/release/EhViewer-universal-A12+.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
app/build/outputs/apk/release/EhViewer-x86-A12+.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
- name: Collect Apks
run: |
mkdir app/build/outputs/apk/release/fdroid
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: cpina/github-action-push-to-another-repository@v1.5
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: app/build/outputs/apk/release/fdroid
destination-github-username: Ehviewer-Overhauled
destination-repository-name: fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
+45 -14
View File
@@ -1,5 +1,17 @@
<p align="right">
<strong>English<strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="art/launcher_icon-web.png" width="200">
<img src="art/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
@@ -7,7 +19,7 @@
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
@@ -21,24 +33,46 @@
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
License
</a>
</h3>
</div>
# Description
致力于轻量化与良好性能表现的 EhViewer 分支
已针对性适配 Material Design 3 及其动态取色系统
EhViewer fork dedicated to lightweight and high-performance
with Material Design 3 and Dynamic Color Support
Maintained by Tarsin Norbin aka 飛鳥澪 (asuka-mio)
Also thanks @txyyh for her help in docs, release notes, design and 1.7.x.x maintenance
# Download
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="art/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="art/get-it-on-fdroid.svg" width="200px"/></a>
App 分为 1.8 与 1.7 两个版本。1.8 版本适配 Android 12 及以上系统,为开发主线;1.7 版本适配 Android 11 及以下版本,削减了 Android 12 及以上系统的独有功能,使其能在低版本 Android 上正常工作
App is divided into two versions: 1.7 and 1.8 . Version 1.8 support Android 12 and above, also the develop mainline. Version 1.7 support Android 11 and below, drop down some spicetify functions for Android 12 to make app useable on lower Android System
There are two development branches: 1.8.x.x and 1.7.x.x . Version 1.8 only support platform api 31 (aka Android S) and above, also the develop mainline. Version 1.7 should only be used on Android 11 and below, since these platforms lack native api of image decoding.
# Screenshot
@@ -47,12 +81,9 @@ App is divided into two versions: 1.7 and 1.8 . Version 1.8 support Android 12 a
# Thanks
本项目受到了诸多开源项目的帮助
Here is the libraries
- [AOSP&AndroidX](http://source.android.com/)
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
- [AOSP & AndroidX](http://source.android.com/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [GnuTLS](https://gnutls.org/)
- [material-components-android](https://github.com/material-components/material-components-android)
+57 -39
View File
@@ -7,12 +7,7 @@ plugins {
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
ndkVersion "25.0.8775105"
Properties localProperties = new Properties()
if (project.rootProject.file('local.properties').exists()) {
localProperties.load(project.rootProject.file('local.properties').newDataInputStream())
}
ndkVersion "25.1.8937393"
splits {
abi {
@@ -34,17 +29,11 @@ android {
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 31
minSdkVersion 28
targetSdkVersion 33
versionCode 180005
versionName "1.8.1.0-alpha01"
versionCode 180021
versionName "1.8.3.0"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=none'
}
}
}
externalNativeBuild {
@@ -54,12 +43,16 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_19
targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '19'
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
]
}
lint {
@@ -92,46 +85,63 @@ android {
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
}
namespace 'com.hippo.ehviewer'
}
dependencies {
def activity_version = "1.6.0-alpha05"
def activity_version = "1.6.1"
implementation "androidx.activity:activity:$activity_version"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.annotation:annotation-experimental:1.2.0"
implementation "androidx.appcompat:appcompat:1.6.0-alpha05"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.annotation:annotation-experimental:1.3.0"
implementation "androidx.appcompat:appcompat:1.6.0-rc01"
def arch_core_version = "2.1.0"
implementation "androidx.arch.core:core-common:$arch_core_version"
implementation "androidx.arch.core:core-runtime:$arch_core_version"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
def collection_version = "1.3.0-alpha02"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
def compose_version = "1.3.1"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-saveable:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.0.1"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha04"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
def core_version = "1.9.0-alpha05"
def core_version = "1.9.0"
implementation "androidx.core:core:$core_version"
implementation "androidx.core:core-ktx:$core_version"
implementation "androidx.customview:customview:1.2.0-alpha01"
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
implementation "androidx.customview:customview:1.2.0-alpha02"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.5.1"
def fragment_version = "1.6.0-alpha03"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
def lifecycle_version = "2.6.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
def lifecycle_version = "2.6.0-alpha03"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
@@ -140,18 +150,21 @@ dependencies {
implementation "androidx.loader:loader:1.1.0"
def nav_version = "2.5.1"
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
implementation "androidx.paging:paging-runtime:3.2.0-alpha03"
def room_version = "2.5.0-alpha02"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
def room_version = "2.5.0-beta02"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
@@ -160,21 +173,26 @@ dependencies {
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.5.0-beta01'
implementation 'androidx.webkit:webkit:1.5.0'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.7.0-alpha03'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
implementation 'com.google.android.material:material:1.7.0'
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'org.jsoup:jsoup:1.15.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'androidx.compose.material'
}
+8 -19
View File
@@ -7,30 +7,19 @@
public static void throw*(...);
}
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keep class com.hippo.ehviewer.dao.* { *; }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
-mergeinterfacesaggressively
-overloadaggressively
-dontwarn net.sqlcipher.database.**
-dontwarn rx.**
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn net.sqlcipher.Cursor
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-keepclasseswithmembernames class * { native <methods>;}
+5 -3
View File
@@ -23,14 +23,14 @@
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Required to maintain app compatibility. -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-feature
android:glEsVersion="0x00020000"
@@ -41,11 +41,13 @@
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
+4 -7
View File
@@ -1,7 +1,7 @@
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -flto \
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
@@ -44,10 +44,7 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
# Build libarchive
add_subdirectory(libarchive)
# Build hippo's image lib
add_subdirectory(image)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED main.c archive.c)
target_include_directories(${PROJECT_NAME} PUBLIC image/image libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} image archive_static -Wl,--gc-sections)
add_library(${PROJECT_NAME} SHARED archive.c image.c fdutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv2 -Wl,--gc-sections)
+251 -84
View File
@@ -17,31 +17,40 @@
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <jni.h>
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <syscall.h>
#include <jni.h>
#include <android/log.h>
#include <archive.h>
#include <archive_entry.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdbool.h>
#define LOG_TAG "libarchive_wrapper"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
static JNIEnv *env;
static int cur_index = 0; // Current entry we haven't read any data from it yet
static struct archive *arc = NULL;
#include "ehviewer.h"
typedef struct {
int using;
int next_index;
struct archive *arc;
struct archive_entry *entry;
} archive_ctx;
pthread_mutex_t ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
static jlong archiveSize = 0;
static size_t archiveSize = 0;
const char supportExt[9][6] = {
"jpeg",
@@ -55,7 +64,7 @@ const char supportExt[9][6] = {
"heif"
};
static int filename_is_playable_file(const char *name) {
static inline int filename_is_playable_file(const char *name) {
const char *dotptr = strrchr(name, '.');
if (!dotptr++)
return false;
@@ -66,52 +75,155 @@ static int filename_is_playable_file(const char *name) {
return false;
}
static long archive_list_all_entries() {
struct archive_entry *entry;
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
if (filename_is_playable_file(archive_entry_pathname(entry)))
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
count++;
}
if (!count)
LOGE("%s", archive_error_string(arc));
LOGE("%s", archive_error_string(ctx->arc));
return count;
}
static void archive_release() {
if (arc) {
archive_read_close(arc);
archive_read_free(arc);
arc = NULL;
static void archive_release_ctx(archive_ctx *ctx) {
if (ctx) {
archive_read_close(ctx->arc);
archive_read_free(ctx->arc);
free(ctx);
}
}
static int archive_alloc() {
archive_release();
arc = archive_read_new();
archive_read_support_format_all(arc);
archive_read_support_filter_all(arc);
static int archive_alloc_ctx(archive_ctx **ctxptr) {
archive_ctx *ctx;
int err;
ctx = calloc(1, sizeof(archive_ctx));
if (!ctx)
return -ENOMEM;
ctx->arc = archive_read_new();
ctx->using = 1;
if (!ctx->arc) {
free(ctx);
return -ENOMEM;
}
archive_read_support_format_tar(ctx->arc);
archive_read_support_format_7zip(ctx->arc);
archive_read_support_format_rar5(ctx->arc);
archive_read_support_format_zip(ctx->arc);
archive_read_support_filter_gzip(ctx->arc);
archive_read_support_filter_xz(ctx->arc);
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(arc, passwd);
return archive_read_open_memory(arc, archiveAddr, archiveSize);
archive_read_add_passphrase(ctx->arc, passwd);
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err) {
archive_read_free(ctx->arc);
free(ctx);
return err;
}
*ctxptr = ctx;
return 0;
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
assert(!(ctx->next_index > index));
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
continue;
if (ctx->next_index++ == index) {
return ctx->next_index - 1;
}
}
return ARCHIVE_FATAL;
}
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
if (ctx_pool[i]->using)
continue;
if (ctx_pool[i]->next_index > idx)
continue;
if (!ctx || ctx_pool[i]->next_index > ctx->next_index)
ctx = ctx_pool[i];
if (ctx->next_index == idx)
break;
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&ctx_lock);
if (!ctx) {
archive_ctx *victimCtx = NULL;
int victimIdx = 0;
int replace = 1;
ret = archive_alloc_ctx(&ctx);
if (ret)
return ret;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
replace = 0;
break;
}
if (ctx_pool[i]->using)
continue;
if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {
victimCtx = ctx_pool[i];
victimIdx = i;
}
}
if (replace) {
archive_release_ctx(victimCtx);
ctx_pool[victimIdx] = ctx;
}
pthread_mutex_unlock(&ctx_lock);
}
ret = archive_skip_to_index(ctx, idx);
if (ret != idx) {
ret = archive_errno(ctx->arc);
LOGE("Skip to index failed:%s", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return ret;
}
*ctxptr = ctx;
return 0;
}
JNIEXPORT jint JNICALL
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *_, jobject thiz, jlong addr, jlong size) {
archiveAddr = (void *) addr;
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
archiveAddr = mmap64(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
return 0;
}
archiveSize = size;
env = _;
madvise(archiveAddr, archiveSize, MADV_SEQUENTIAL | MADV_WILLNEED);
long r = archive_alloc();
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
pthread_mutex_init(&ctx_lock, 0);
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
LOGE("Allocate archive ctx pool failed:ENOMEM");
return 0;
}
long r = archive_alloc_ctx(&ctx);
if (r) {
if (r == -ENOMEM)
LOGE("%s", "Mem alloc failed");
r = 0;
LOGE("%s%s", "Archive open failed:", archive_error_string(arc));
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
r = archive_list_all_entries();
r = archive_list_all_entries(ctx);
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(arc);
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
@@ -120,77 +232,132 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *_, jobject thiz, jlong add
default:
need_encrypt = false;
}
LOGI("%s%d", "archive_read_has_encrypted_entries returns ", encryptRet);
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR_V5:
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
}
archive_release();
archive_release_ctx(ctx);
return r;
}
typedef struct Memarea {
void *buffer;
long size;
} Memarea;
JNIEXPORT jlong JNICALL
Java_com_hippo_UriArchiveAccessor_extracttoOutputStream(JNIEnv *_, jobject thiz, jint index) {
struct archive_entry *entry;
JNIEXPORT jobject JNICALL
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
if (!arc || index < cur_index) {
archive_alloc();
cur_index = 0;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size);
if (!buffer) {
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
}
Memarea *memarea = malloc(sizeof(Memarea));
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(entry)))
continue;
if (cur_index++ == index) {
memarea->size = archive_entry_size(entry);
memarea->buffer = malloc(memarea->size);
ret = archive_read_data(arc, memarea->buffer, memarea->size);
if (ret != memarea->size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(arc));
break;
}
}
return (jlong) memarea;
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(buffer);
return 0;
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *jniEnv, jobject thiz) {
archive_release();
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(passwd);
free(ctx_pool);
pthread_mutex_destroy(&ctx_lock);
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *_, jobject thiz) {
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *_, jobject thiz, jstring str) {
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
archive_ctx *ctx;
jboolean ret = true;
int len = (*env)->GetStringUTFLength(env, str);
if (passwd)
free(passwd);
passwd = calloc(len, sizeof(char));
passwd = realloc(passwd, len * sizeof(char));
if (!passwd) {
LOGE("Allocate passwd buffer failed");
return false;
}
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
archive_alloc();
while (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
archive_alloc_ctx(&ctx);
void *tmpBuf = alloca(4096);
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(entry)))
continue;
if (!archive_entry_is_encrypted(entry))
continue;
void *tmpBuf = malloc(4096);
if (archive_read_data(arc, tmpBuf, 4096) < ARCHIVE_OK) {
LOGE("%s%s", "Archive read failed:", archive_error_string(arc));
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
ret = false;
}
free(tmpBuf);
break;
}
archive_release();
archive_release_ctx(ctx);
return ret;
}
JNIEXPORT jstring JNICALL
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return NULL;
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
ctx->using = 0;
return str;
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (!ret) {
archive_read_data_into_fd(ctx->arc, fd);
ctx->using = 0;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
free(addr);
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EHVIEWER_EHVIEWER_H
#define EHVIEWER_EHVIEWER_H
#define EH_UNUSED(x) (void)x
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
#define madvise_log_if_error(addr, len, advice) \
if (madvise(addr, len, advice)) \
LOGE("%s%p%s%zu%s%d%s%s%s", "madvise addr:", addr, "len:", len, "with advice ", advice, " failed with error: ", strerror(errno), ", Ignored")
#endif /* EHVIEWER_EHVIEWER_H */
+16
View File
@@ -0,0 +1,16 @@
#include <jni.h>
JNIEXPORT jint JNICALL
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
jint fd = -1;
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
if (fdClass != NULL) {
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
}
}
return fd;
}
+166
View File
@@ -0,0 +1,166 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include <android/bitmap.h>
#include <android/data_space.h>
#include <android/log.h>
#include <GLES2/gl2.h>
#include <jni.h>
#define TAG "ImageDecoder_wrapper"
#include "ehviewer.h"
#define IMAGE_TILE_MAX_SIZE (512 * 512)
static char tile_buffer[IMAGE_TILE_MAX_SIZE * 4];
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
int left;
int line;
size_t line_stride;
int src_stride;
int src_pos;
int dst_pos;
size_t dst_blank_length;
// Sanitize
if (src_x < 0) {
width -= src_x;
dst_x -= src_x;
src_x = 0;
}
if (dst_x < 0) {
width -= dst_x;
src_x -= dst_x;
dst_x = 0;
}
if (width <= 0) {
return false;
}
if (src_y < 0) {
height -= src_y;
dst_y -= src_y;
src_y = 0;
}
if (dst_y < 0) {
height -= dst_y;
src_y -= dst_y;
dst_y = 0;
}
if (height <= 0) {
return false;
}
left = src_x + width - src_w;
if (left > 0) {
width -= left;
}
left = dst_x + width - dst_w;
if (left > 0) {
width -= left;
}
if (width <= 0) {
return false;
}
left = src_y + height - src_h;
if (left > 0) {
height -= left;
}
left = dst_y + height - dst_h;
if (left > 0) {
height -= left;
}
if (height <= 0) {
return false;
}
// Init
line_stride = (size_t) (width * 4);
src_stride = src_w * 4;
src_pos = src_y * src_stride + src_x * 4;
dst_pos = 0;
dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * 4;
// First line
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
// Other lines
dst_blank_length = (size_t) ((dst_w - width) * 4);
for (line = 1; line < height; line++) {
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
}
return true;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jobject srcBitmap, jint src_x,
jint src_y, jobject dst, jint dst_x, jint dst_y, jint width,
jint height) {
AndroidBitmapInfo dstInfo;
AndroidBitmapInfo srcInfo;
void *srcPixels = NULL;
void *dstPixels = NULL;
AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);
AndroidBitmap_lockPixels(env, dst, &dstPixels);
AndroidBitmap_getInfo(env, srcBitmap, &srcInfo);
AndroidBitmap_getInfo(env, dst, &dstInfo);
copy_pixels(srcPixels, srcInfo.width, srcInfo.height, src_x, src_y, dstPixels, dstInfo.width,
dstInfo.height, dst_x, dst_y,
width,
height);
AndroidBitmap_unlockPixels(env, dst);
AndroidBitmap_unlockPixels(env, srcBitmap);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init,
jint offset_x, jint offset_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_lockPixels(env, bitmap, &pixels);
AndroidBitmap_getInfo(env, bitmap, &info);
copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height);
AndroidBitmap_unlockPixels(env, bitmap);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
}
}
-34
View File
@@ -1,34 +0,0 @@
# Copyright 2018 Hippo Seven
#
# 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.
cmake_minimum_required(VERSION 3.4.1)
project(image C)
set(IMAGE_SOURCES
image.c
image_utils.c
java_wrapper.c
)
set(IMAGE_INCLUDES
${CMAKE_CURRENT_SOURCE_DIR}
)
set(IMAGE_LIBRARIES
log
jnigraphics
GLESv2
)
add_library(image STATIC ${IMAGE_SOURCES})
target_include_directories(image PRIVATE ${IMAGE_INCLUDES})
target_link_libraries(image PRIVATE ${IMAGE_LIBRARIES})
-112
View File
@@ -1,112 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <android/data_space.h>
#include <stdlib.h>
#include <string.h>
#include "image.h"
#include "image_utils.h"
void decodeCommon(IMAGE *image, SOURCE *src) {
image->src = src;
AImageDecoder *decoder = src->decoder;
image->isAnimated = AImageDecoder_isAnimated(decoder);
AImageDecoder_setDataSpace(decoder, ADATASPACE_SRGB);
AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888);
const AImageDecoderHeaderInfo *headerInfo = AImageDecoder_getHeaderInfo(decoder);
image->height = AImageDecoderHeaderInfo_getHeight(headerInfo);
image->width = AImageDecoderHeaderInfo_getWidth(headerInfo);
image->alpha =
AImageDecoderHeaderInfo_getAlphaFlags(headerInfo) == ANDROID_BITMAP_FLAGS_ALPHA_PREMUL;
src->stride = AImageDecoder_getMinimumStride(decoder);
image->bufferLen = (int) (image->height * src->stride);
image->buffer = malloc(image->bufferLen);
if (image->isAnimated) {
AImageDecoder_setInternallyHandleDisposePrevious(decoder, false);
AImageDecoderFrameInfo *frameInfo = AImageDecoderFrameInfo_create();
AImageDecoder_getFrameInfo(decoder, frameInfo);
image->delay = (int) AImageDecoderFrameInfo_getDuration(frameInfo) / 1000000;
AImageDecoderFrameInfo_delete(frameInfo);
}
AImageDecoder_decodeImage(decoder, image->buffer, src->stride, image->bufferLen);
if (!image->isAnimated) {
recycleSource(image->src);
image->src = NULL;
}
}
IMAGE *createFromFd(int fd) {
AImageDecoder *decoder = NULL;
if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS)
return NULL;
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->fd = fd;
decodeCommon(image, src);
return image;
}
IMAGE *createFromAddr(void *addr, long size) {
AImageDecoder *decoder;
AImageDecoder_createFromBuffer((const void *) addr, size, &decoder);
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->srcBuffer = addr;
decodeCommon(image, src);
return image;
}
IMAGE *create(int32_t width, int32_t height, const void *data) {
IMAGE *plain = calloc(1, sizeof(IMAGE));
void *buffer = NULL;
size_t length = width * height * 4;
buffer = malloc(length);
memcpy(buffer, data, length);
// plain->isAnimated = false; this is unnecessary since we use calloc
plain->width = width;
plain->height = height;
plain->buffer = buffer;
plain->bufferLen = (int) length;
return plain;
}
void render(IMAGE *image, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
copy_pixels(image->buffer, image->width, image->height, src_x, src_y, dst, dst_w, dst_h, dst_x,
dst_y, width, height);
}
void advance(IMAGE *image) {
if (image->isAnimated) {
AImageDecoder_advanceFrame(image->src->decoder);
if (AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen) == ANDROID_IMAGE_DECODER_FINISHED) {
AImageDecoder_rewind(image->src->decoder);
AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen);
}
}
}
-72
View File
@@ -1,72 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IMAGE_IMAGE_H
#define IMAGE_IMAGE_H
#include <android/imagedecoder.h>
#include <stdbool.h>
typedef struct {
AImageDecoder *decoder;
void *srcBuffer;
size_t stride;
int fd;
} SOURCE;
typedef struct {
int32_t width;
int32_t height;
void *buffer;
int bufferLen;
bool isAnimated;
bool alpha;
int delay;
SOURCE *src;
} IMAGE;
static void recycleSource(SOURCE *src) {
if (!src)
return;
AImageDecoder_delete(src->decoder);
free(src->srcBuffer);
free(src);
}
static void recycle(IMAGE *image) {
if (!image)
return;
recycleSource(image->src);
free(image->buffer);
free(image);
}
IMAGE *createFromFd(int fd);
IMAGE *createFromAddr(void *addr, long size);
IMAGE *create(int32_t width, int32_t height, const void *data);
void render(IMAGE *image, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height);
void advance(IMAGE *image);
#endif /* IMAGE_IMAGE_H */
-107
View File
@@ -1,107 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* 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.
*/
#include "image_utils.h"
#include <stdlib.h>
#include <string.h>
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
int left;
int line;
size_t line_stride;
int src_stride;
int src_pos;
int dst_pos;
size_t dst_blank_length;
// Sanitize
if (src_x < 0) {
width -= src_x;
dst_x -= src_x;
src_x = 0;
}
if (dst_x < 0) {
width -= dst_x;
src_x -= dst_x;
dst_x = 0;
}
if (width <= 0) {
return false;
}
if (src_y < 0) {
height -= src_y;
dst_y -= src_y;
src_y = 0;
}
if (dst_y < 0) {
height -= dst_y;
src_y -= dst_y;
dst_y = 0;
}
if (height <= 0) {
return false;
}
left = src_x + width - src_w;
if (left > 0) {
width -= left;
}
left = dst_x + width - dst_w;
if (left > 0) {
width -= left;
}
if (width <= 0) {
return false;
}
left = src_y + height - src_h;
if (left > 0) {
height -= left;
}
left = dst_y + height - dst_h;
if (left > 0) {
height -= left;
}
if (height <= 0) {
return false;
}
// Init
line_stride = (size_t) (width * 4);
src_stride = src_w * 4;
src_pos = src_y * src_stride + src_x * 4;
dst_pos = 0;
dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * 4;
// First line
dst_pos += dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += line_stride;
src_pos += src_stride;
// Other lines
dst_blank_length = (size_t) ((dst_w - width) * 4);
for (line = 1; line < height; line++) {
dst_pos += dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += line_stride;
src_pos += src_stride;
}
return true;
}
-29
View File
@@ -1,29 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IMAGE_IMAGE_UTILS_H
#define IMAGE_IMAGE_UTILS_H
#include <stdbool.h>
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height);
#endif /* IMAGE_IMAGE_UTILS_H */
-152
View File
@@ -1,152 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* 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.
*/
#include <stdlib.h>
#include <android/bitmap.h>
#include <GLES2/gl2.h>
#include <jni.h>
#include "java_wrapper.h"
#include "image.h"
#include "log.h"
static void *tile_buffer = NULL;
static int jniGetFDFromFileDescriptor(JNIEnv *env, jobject fileDescriptor) {
jint fd = -1;
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
if (fdClass != NULL) {
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
}
}
return fd;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecodeFdInt(JNIEnv *env, jclass clazz, jint fd) {
IMAGE *image = createFromFd(fd);
return (jlong) image;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecode(JNIEnv *env, jclass clazz, jobject fd) {
return Java_com_hippo_image_Image_nativeDecodeFdInt(env, clazz,
jniGetFDFromFileDescriptor(env, fd));
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeCreate(JNIEnv *env, jclass clazz, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels = NULL;
void *image = NULL;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
image = create(info.width, info.height, pixels);
AndroidBitmap_unlockPixels(env, bitmap);
return (jlong) image;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jlong ptr, jint src_x,
jint src_y, jobject dst, jint dst_x, jint dst_y, jint width,
jint height) {
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_getInfo(env, dst, &info);
AndroidBitmap_lockPixels(env, dst, &pixels);
render((IMAGE *) ptr, src_x, src_y, pixels, info.width, info.height, dst_x, dst_y, width,
height);
AndroidBitmap_unlockPixels(env, dst);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr, jboolean init,
jint src_x, jint src_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
render((IMAGE *) ptr, src_x, src_y, tile_buffer, width, height, 0, 0, width, height);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
}
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeAdvance(JNIEnv *env, jclass clazz, jlong ptr) {
advance((IMAGE *) ptr);
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetDelay(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->delay;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_image_Image_nativeIsOpaque(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->alpha;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRecycle(JNIEnv *env, jclass clazz, jlong ptr) {
recycle((IMAGE *) ptr);
}
bool image_onLoad() {
tile_buffer = malloc(IMAGE_TILE_MAX_SIZE * 4);
return tile_buffer != NULL;
}
void image_onUnload() {
free(tile_buffer);
tile_buffer = NULL;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecodeAddr(JNIEnv *env, jclass clazz, jlong addr) {
Memarea *memarea = (Memarea *) addr;
IMAGE *image = createFromAddr(memarea->buffer, memarea->size);
free(memarea);
return (jlong) image;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetFormat(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->isAnimated;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetWidth(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->width;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetHeight(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->height;
}
-36
View File
@@ -1,36 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IMAGE_JAVA_WRAPPER_H
#define IMAGE_JAVA_WRAPPER_H
#include <stdbool.h>
#define IMAGE_TILE_MAX_SIZE (512 * 512)
typedef struct Memarea {
void *buffer;
long size;
} Memarea;
bool image_onLoad();
void image_onUnload();
#endif /* IMAGE_JAVA_WRAPPER_H */
-65
View File
@@ -1,65 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <jni.h>
#include <archive.h>
#include <lzma.h>
#include <zlib.h>
#include <nettle/config.h>
#include "image/java_wrapper.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
if (!image_onLoad(vm)) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *reserved) {
image_onUnload();
}
JNIEXPORT jstring JNICALL
Java_com_hippo_Native_getlibarchiveVersion(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env, ARCHIVE_VERSION_STRING);
}
JNIEXPORT jstring JNICALL
Java_com_hippo_Native_getliblzmaVersion(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env, lzma_version_string());
}
JNIEXPORT jstring JNICALL
Java_com_hippo_Native_getzlibVersion(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env, ZLIB_VERSION);
}
JNIEXPORT jstring JNICALL
Java_com_hippo_Native_getnettleVersion(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env, PACKAGE_STRING);
}
+2 -2
View File
@@ -149,7 +149,7 @@
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.8"
#define PACKAGE_STRING "nettle 3.8.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
@@ -158,7 +158,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.8"
#define PACKAGE_VERSION "3.8.1"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
@@ -15,20 +15,16 @@
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo
package com.hippo;
import java.io.FileDescriptor
public class Native {
public static void initialize() {
System.loadLibrary("ehviewer");
object Native {
@JvmStatic
fun initialize() {
System.loadLibrary("ehviewer")
}
public static native String getlibarchiveVersion();
public static native String getliblzmaVersion();
public static native String getzlibVersion();
public static native String getnettleVersion();
}
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
}
@@ -1,64 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo;
import android.content.Context;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
public class UriArchiveAccessor {
ParcelFileDescriptor pfd;
FileDescriptor fd;
long archiveAddr;
Context ctx;
Uri uri;
public UriArchiveAccessor(Context ctx, Uri uri) {
this.ctx = ctx;
this.uri = uri;
}
public int open() throws Exception {
pfd = ctx.getContentResolver().openFileDescriptor(uri, "r");
fd = pfd.getFileDescriptor();
archiveAddr = Os.mmap(0, pfd.getStatSize(), OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, fd, 0);
return openArchive(archiveAddr, pfd.getStatSize());
}
private native int openArchive(long addr, long size);
public native long extracttoOutputStream(int index);
public native boolean needPassword();
public native boolean providePassword(String str);
private native void closeArchive();
public void close() throws Exception {
closeArchive();
Os.munmap(archiveAddr, pfd.getStatSize());
Os.close(fd);
pfd.close();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo
import android.content.Context
import android.net.Uri
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
fun open(): Int {
return openArchive(pfd.fd, pfd.statSize)
}
private external fun openArchive(fd: Int, size: Long): Int
external fun extractToByteBuffer(index: Int): ByteBuffer?
external fun releaseByteBuffer(buffer: ByteBuffer)
external fun extractToFd(index: Int, fd: Int)
external fun getFilename(index: Int): String
external fun needPassword(): Boolean
external fun providePassword(str: String): Boolean
private external fun closeArchive()
fun close() {
closeArchive()
pfd.close()
}
}
@@ -0,0 +1,23 @@
package com.hippo.app
import android.content.Context
import android.os.Build
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
open class BaseDialogBuilder constructor(
context: Context
) : MaterialAlertDialogBuilder(context) {
override fun create(): AlertDialog {
return super.create().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window?.run {
addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes.blurBehindRadius = 32
}
}
}
}
}
@@ -18,7 +18,6 @@ package com.hippo.app
import android.content.Context
import android.view.LayoutInflater
import android.widget.CheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.ehviewer.R
class CheckBoxDialogBuilder constructor(
@@ -26,7 +25,7 @@ class CheckBoxDialogBuilder constructor(
message: String?,
checkText: String?,
checked: Boolean
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
) {
private val mCheckBox: CheckBox
@@ -23,7 +23,6 @@ import android.widget.EditText
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
@@ -31,7 +30,7 @@ class EditTextDialogBuilder constructor(
context: Context,
text: String?,
hint: String?
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
), OnEditorActionListener {
private val mTextInputLayout: TextInputLayout
@@ -0,0 +1,14 @@
package com.hippo.app
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import com.google.android.material.slider.Slider
class HapticSlider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Slider(context, attrs) {
init {
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
}
}
@@ -23,14 +23,13 @@ import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.ehviewer.R
import com.hippo.yorozuya.ViewUtils
class ListCheckBoxDialogBuilder constructor(
context: Context, items: Array<CharSequence>,
listener: OnItemClickListener?, checkText: String?, checked: Boolean
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
) {
private val mCheckBox: CheckBox
@@ -231,6 +231,21 @@ public final class DiskLruCache implements Closeable {
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
@@ -247,21 +262,6 @@ public final class DiskLruCache implements Closeable {
}
};
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
}
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
@@ -23,7 +23,6 @@ import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
@@ -140,7 +139,7 @@ class StrictLineReader implements Closeable {
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
String res = new String(buf, pos, lineEnd - pos, charset.name());
String res = new String(buf, pos, lineEnd - pos, charset);
pos = i + 1;
return res;
}
@@ -152,11 +151,7 @@ class StrictLineReader implements Closeable {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e); // Since we control the charset this will never happen.
}
return new String(buf, 0, length, charset);
}
};
@@ -25,7 +25,6 @@ import androidx.annotation.UiThread;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.okhttp.ChromeRequestBuilder;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.IOException;
@@ -35,8 +34,6 @@ import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -51,7 +48,6 @@ public final class ConacoTask<V> {
private final WeakReference<Unikery<V>> mUnikeryWeakReference;
private final String mKey;
private final String mUrl;
private final DataContainer mDataContainer;
private final boolean mUseMemoryCache;
private final boolean mUseDiskCache;
private final boolean mUseNetwork;
@@ -73,7 +69,6 @@ public final class ConacoTask<V> {
mUnikeryWeakReference = new WeakReference<>(builder.mUnikery);
mKey = builder.mKey;
mUrl = builder.mUrl;
mDataContainer = builder.mDataContainer;
mUseMemoryCache = builder.mUseMemoryCache;
mUseDiskCache = builder.mUseDiskCache;
mUseNetwork = builder.mUseNetwork;
@@ -85,42 +80,6 @@ public final class ConacoTask<V> {
mConaco = builder.mConaco;
}
private static void putFromDiskCacheToDataContainer(String key, ValueCache cache, DataContainer container) {
SimpleDiskCache diskCache = cache.getDiskCache();
if (diskCache != null) {
InputStreamPipe pipe = diskCache.getInputStreamPipe(key);
if (pipe != null) {
try {
pipe.obtain();
container.save(pipe.open(), -1L, null, null);
} catch (IOException e) {
Log.d(TAG, "Can't save value from disk cache to data container");
e.printStackTrace();
container.remove();
} finally {
pipe.close();
pipe.release();
}
}
}
}
private static void putFromDataContainerToDiskCache(String key, ValueCache cache, DataContainer container) {
InputStreamPipe pipe = container.get();
if (pipe != null) {
try {
pipe.obtain();
cache.putRawToDisk(key, pipe.open());
} catch (IOException e) {
Log.w(TAG, "Can't save value from data container to disk cache", e);
cache.removeFromDisk(key);
} finally {
pipe.close();
pipe.release();
}
}
}
int getId() {
return mId;
}
@@ -194,7 +153,6 @@ public final class ConacoTask<V> {
} else if (mNetworkLoadTask != null) { // Getting from network
mNetworkLoadTask.cancel(false);
if (mCall != null) {
mCall.cancel();
mCall = null;
}
}
@@ -219,7 +177,6 @@ public final class ConacoTask<V> {
private Unikery<T> mUnikery;
private String mKey;
private String mUrl;
private DataContainer mDataContainer;
private boolean mUseMemoryCache = true;
private boolean mUseDiskCache = true;
private boolean mUseNetwork = true;
@@ -262,11 +219,6 @@ public final class ConacoTask<V> {
return this;
}
public Builder<T> setDataContainer(DataContainer dataContainer) {
mDataContainer = dataContainer;
return this;
}
boolean isUseMemoryCache() {
return mUseMemoryCache;
}
@@ -332,7 +284,7 @@ public final class ConacoTask<V> {
if (mUnikery == null) {
throw new IllegalStateException("Must set unikery");
}
if (mKey == null && mUrl == null && mDataContainer == null) {
if (mKey == null && mUrl == null) {
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
}
}
@@ -352,26 +304,13 @@ public final class ConacoTask<V> {
} else {
V value = null;
// First check data container
if (mDataContainer != null && mDataContainer.isEnabled()) {
InputStreamPipe isp = mDataContainer.get();
if (isp != null) {
value = mHelper.decode(isp);
}
}
// Then check disk cache
if (mKey != null) {
if (value == null && mUseDiskCache) {
value = mCache.getFromDisk(mKey);
// Put back to data container
if (value != null && mDataContainer != null && mDataContainer.isEnabled()) {
putFromDiskCacheToDataContainer(mKey, mCache, mDataContainer);
}
}
if (value != null && mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
}
@@ -451,18 +390,6 @@ public final class ConacoTask<V> {
}
}
private boolean putToDataContainer(InputStream is, ResponseBody body) {
// Get media type
String mediaType;
MediaType mt = body.contentType();
if (mt != null) {
mediaType = mt.type() + '/' + mt.subtype();
} else {
mediaType = null;
}
return mDataContainer.save(is, body.contentLength(), mediaType, this);
}
@Override
protected V doInBackground(Void... params) {
if (isNotNecessary(this)) {
@@ -489,7 +416,7 @@ public final class ConacoTask<V> {
return null;
}
if ((mDataContainer == null || !mDataContainer.isEnabled()) && mKey != null) {
if (mKey != null) {
if (putToDiskCache(is, body.contentLength())) {
// Get object from disk cache
value = mCache.getFromDisk(mKey);
@@ -506,37 +433,6 @@ public final class ConacoTask<V> {
mCache.removeFromDisk(mKey);
return null;
}
} else if (mDataContainer != null && mDataContainer.isEnabled()) {
// Check url Moved
HttpUrl requestHttpUrl = request.url();
HttpUrl responseHttpUrl = response.request().url();
if (!responseHttpUrl.equals(requestHttpUrl)) {
mDataContainer.onUrlMoved(mUrl, responseHttpUrl.url().toString());
}
// Put to data container
if (!putToDataContainer(is, body)) {
return null;
}
// Get value from data container
InputStreamPipe isp = mDataContainer.get();
if (isp == null) {
return null;
}
value = mHelper.decode(isp);
if (value == null) {
mDataContainer.remove();
} else if (mKey != null) {
// Put to disk cache
putFromDataContainerToDiskCache(mKey, mCache, mDataContainer);
if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
}
return value;
} else {
return null;
}
@@ -1,52 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* 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 com.hippo.conaco;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import java.io.InputStream;
public interface DataContainer {
/**
* Returns the enabled status for this DataContainer.
*/
boolean isEnabled();
/**
* Get 304 or something like that
*/
void onUrlMoved(String requestUrl, String responseUrl);
/**
* Save the {@code InputStream}
*/
boolean save(InputStream is, long length, @Nullable String mediaType, @Nullable ProgressNotifier notify);
/**
* Get {@code InputStreamPipe} for saved before
*/
@Nullable
InputStreamPipe get();
/**
* Remove saved stuff
*/
void remove();
}
@@ -16,6 +16,7 @@
package com.hippo.drawable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
@@ -25,7 +26,6 @@ import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.Unikery;
import com.hippo.image.ImageBitmap;
import com.hippo.image.ImageDrawable;
import com.hippo.widget.ObservedTextView;
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
@@ -37,6 +37,8 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
private int mTaskId = Unikery.INVALID_ID;
private String mUrl;
private ImageBitmap imageBitmap;
public UnikeryDrawable(ObservedTextView textView, Conaco<ImageBitmap> conaco) {
mTextView = textView;
mTextView.setOnWindowAttachListener(this);
@@ -62,11 +64,11 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
}
private void clearDrawable() {
Drawable drawable = getDrawable();
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).recycle();
}
setDrawable(null);
if (imageBitmap != null) {
imageBitmap.release();
imageBitmap = null;
}
}
@Override
@@ -123,9 +125,9 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
@Override
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
ImageDrawable drawable;
Drawable drawable;
try {
drawable = new ImageDrawable(value);
drawable = value.getDrawable();
} catch (Exception e) {
Log.d(TAG, "The ImageBitmap is recycled", e);
return false;
@@ -134,7 +136,9 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
clearDrawable();
setDrawable(drawable);
drawable.start();
imageBitmap = value;
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
animatedImageDrawable.start();
return true;
}
@@ -64,8 +64,7 @@ public final class LayoutManagerUtils {
RecyclerView.LayoutManager layoutManager, Context context, int position,
int millisecondsPerInch) {
SimpleSmoothScroller smoothScroller;
if (layoutManager instanceof LinearLayoutManager) {
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (layoutManager instanceof final LinearLayoutManager linearLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -73,8 +72,7 @@ public final class LayoutManagerUtils {
}
};
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
} else if (layoutManager instanceof final StaggeredGridLayoutManager staggeredGridLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -26,6 +26,7 @@ import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Debug;
import android.text.Html;
import android.text.method.LinkMovementMethod;
@@ -43,8 +44,8 @@ import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.Native;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.conaco.Conaco;
import com.hippo.ehviewer.client.EhClient;
@@ -60,13 +61,12 @@ import com.hippo.ehviewer.client.parser.EventPaneParser;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.image.Image;
import com.hippo.ehviewer.ui.EhActivity;
import com.hippo.image.ImageBitmap;
import com.hippo.network.StatusCodeException;
import com.hippo.scene.SceneApplication;
import com.hippo.unifile.UniFile;
import com.hippo.util.BitmapUtils;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.util.ReadableTime;
@@ -108,6 +108,7 @@ public class EhApplication extends SceneApplication {
private static final long DEBUG_PRINT_INTERVAL = 3000L;
public static boolean locked = true;
public static long locked_last_leave_time = 0;
private static EhApplication instance;
@@ -185,10 +186,9 @@ public class EhApplication extends SceneApplication {
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager trustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
builder.sslSocketFactory(new EhSSLSocketFactory(), trustManager);
} catch (Exception e) {
e.printStackTrace();
@@ -326,7 +326,6 @@ public class EhApplication extends SceneApplication {
super.onCreate();
Native.initialize();
ClipboardUtil.initialize(this);
GetText.initialize(this);
StatusCodeException.initialize(this);
Settings.initialize(this);
@@ -337,7 +336,10 @@ public class EhApplication extends SceneApplication {
EhEngine.initialize();
BitmapUtils.initialize(this);
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
// Locales can be managed by system automatically above Snow Cone v2
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
// Do io tasks in new thread
@@ -425,7 +427,7 @@ public class EhApplication extends SceneApplication {
Activity activity = getTopActivity();
if (activity != null) {
activity.runOnUiThread(() -> {
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
AlertDialog dialog = new BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html))
.setPositiveButton(android.R.string.ok, null)
.create();
@@ -488,9 +490,6 @@ public class EhApplication extends SceneApplication {
Log.i(TAG, "Native memory: " + FileUtils.humanReadableByteCount(
Debug.getNativeHeapAllocatedSize(), false));
}
if (DEBUG_PRINT_IMAGE_COUNT) {
Log.i(TAG, "Image count: " + Image.getImageCount());
}
SimpleHandler.getInstance().postDelayed(this, DEBUG_PRINT_INTERVAL);
}
}.run();
@@ -528,9 +527,9 @@ public class EhApplication extends SceneApplication {
}
@Nullable
public Activity getTopActivity() {
public EhActivity getTopActivity() {
if (!mActivityList.isEmpty()) {
return mActivityList.get(mActivityList.size() - 1);
return (EhActivity) mActivityList.get(mActivityList.size() - 1);
} else {
return null;
}
@@ -586,6 +585,8 @@ public class EhApplication extends SceneApplication {
static class EhlifecycleObserver implements DefaultLifecycleObserver {
@Override
public void onPause(@NonNull LifecycleOwner owner) {
if (!locked)
locked_last_leave_time = System.currentTimeMillis() / 1000;
locked = true;
}
}
+61 -43
View File
@@ -22,6 +22,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.room.Room;
import com.hippo.ehviewer.client.data.GalleryInfo;
@@ -57,8 +58,6 @@ import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static final int MAX_HISTORY_COUNT = 200;
private static EhDatabase db;
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
@@ -328,10 +327,14 @@ public class EhDB {
dao.update(list);
}
public static synchronized List<HistoryInfo> getHistoryLazyList() {
public static synchronized List<HistoryInfo> getHistoryList() {
return db.historyDao().list();
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = db.historyDao();
HistoryInfo info = dao.load(galleryInfo.gid);
@@ -344,8 +347,6 @@ public class EhDB {
info = new HistoryInfo(galleryInfo);
info.time = System.currentTimeMillis();
dao.insert(info);
List<HistoryInfo> list = dao.list(MAX_HISTORY_COUNT, -1);
dao.delete(list);
}
}
@@ -356,9 +357,6 @@ public class EhDB {
dao.insert(info);
}
}
List<HistoryInfo> list = dao.list(MAX_HISTORY_COUNT, -1);
dao.delete(list);
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
@@ -492,59 +490,79 @@ public class EhDB {
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
.createFromFile(file).allowMainThreadQueries().build();
// Download label
DownloadManager manager = EhApplication.getDownloadManager(context);
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
} catch (Exception ignored) {
}
// Downloads
DownloadManager manager = EhApplication.getDownloadManager(context);
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
// Download label
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
try {
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
} catch (Exception ignored) {
}
// Download dirname
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
try {
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {
}
// History
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {
}
// QuickSearch
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.name;
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.name, name)) {
// The same name
name = null;
break;
try {
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.name;
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.name, name)) {
// The same name
name = null;
break;
}
}
if (null == name) {
continue;
}
importList.add(quickSearch);
}
if (null == name) {
continue;
}
importList.add(quickSearch);
importQuickSearch(importList);
} catch (Exception ignored) {
}
importQuickSearch(importList);
// LocalFavorites
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
try {
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {
}
// Filter
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
try {
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
}
}
} catch (Exception ignored) {
}
return null;
@@ -36,7 +36,7 @@ public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
try {
isPipe.obtain();
FileInputStream is = (FileInputStream) isPipe.open();
return ImageBitmap.decode(is.getFD());
return ImageBitmap.decode(is);
} catch (OutOfMemoryError | IOException e) {
return null;
} finally {
@@ -50,6 +50,8 @@ public class Settings {
********************/
public static final String KEY_THEME = "theme";
public static final String KEY_ACCOUNT = "account";
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
public static final int THEME_LIGHT = 1;
public static final int THEME_SYSTEM = -1;
@@ -76,6 +78,8 @@ public class Settings {
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
public static final String KEY_MEDIA_SCAN = "media_scan";
@@ -88,6 +92,7 @@ public class Settings {
********************/
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
public static final String KEY_SECURITY = "require_unlock";
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
public static final String DEFAULT_SECURITY = "";
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
@@ -95,11 +100,13 @@ public class Settings {
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_APP_LANGUAGE = "app_language";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_VERSION_CODE = "version_code";
private static final int DEFAULT_VERSION_CODE = 0;
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
private static final String KEY_AVATAR = "avatar";
private static final String DEFAULT_AVATAR = null;
private static final String KEY_SHOW_WARNING = "show_warning";
@@ -121,8 +128,6 @@ public class Settings {
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 1;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_FIX_THUMB_URL = "fix_thumb_url";
private static final boolean DEFAULT_FIX_THUMB_URL = false;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
@@ -272,11 +277,9 @@ public class Settings {
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
public static boolean LIST_THUMB_SIZE_INITED = false;
private static int LIST_THUMB_SIZE = 40;
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static EhConfig sEhConfig;
@@ -426,6 +429,7 @@ public class Settings {
putInt(KEY_VERSION_CODE, value);
}
@Nullable
public static String getDisplayName() {
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
}
@@ -553,10 +557,6 @@ public class Settings {
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
}
public static boolean getFixThumbUrl() {
return getBoolean(KEY_FIX_THUMB_URL, DEFAULT_FIX_THUMB_URL);
}
public static boolean getRequestNews() {
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
}
@@ -968,11 +968,11 @@ public class Settings {
boolean writeFile = false;
String userID = getString(KEY_USER_ID, null);
File file = AppConfig.getFileInExternalAppDir(FILENAME_USER_ID);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeXml = true;
// Get use ID from out sd card file
userID = FileUtils.read(file);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeFile = true;
userID = generateUserID();
}
@@ -1057,6 +1057,10 @@ public class Settings {
putBoolean(KEY_SECURITY, value);
}
public static int getSecurityDelay() {
return getInt(KEY_SECURITY_DELAY, 0);
}
public static boolean getEnableFingerprint() {
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
}
@@ -1168,4 +1172,22 @@ public class Settings {
public static void putPasswdToArchivePasswds(String value) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
}
public static int getListThumbSize() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE;
}
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
LIST_THUMB_SIZE = size;
LIST_THUMB_SIZE_INITED = true;
return size;
}
public static boolean getNotificationRequired() {
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
}
public static void putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
}
}
@@ -245,12 +245,6 @@ public class EhEngine {
}
}
}
if (Settings.getFixThumbUrl()) {
for (GalleryInfo info : list) {
info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb);
}
}
}
public static GalleryListParser.Result getGalleryList(@Nullable EhClient.Task task, OkHttpClient okHttpClient,
@@ -21,11 +21,6 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.Settings;
import com.hippo.network.UrlBuilder;
import java.util.List;
import java.util.ListIterator;
import okhttp3.HttpUrl;
public class EhUrl {
public static final int SITE_E = 0;
@@ -59,8 +54,6 @@ public class EhUrl {
public static final String ORIGIN_EX = REFERER_EX;
public static final String REFERER_E = "https://" + DOMAIN_E;
public static final String ORIGIN_E = REFERER_E;
private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/";
private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/";
public static String getGalleryDetailUrl(long gid, String token) {
return getGalleryDetailUrl(gid, token, 0, false);
@@ -200,44 +193,4 @@ public class EhUrl {
return URL_WATCHED_EX;
}
}
public static String getThumbUrlPrefix() {
switch (Settings.getGallerySite()) {
default:
//case SITE_E:
return URL_PREFIX_THUMB_E;
//case SITE_EX:
// return URL_PREFIX_THUMB_EX;
}
}
public static String getFixedPreviewThumbUrl(String originUrl) {
HttpUrl url = HttpUrl.parse(originUrl);
if (url == null) return originUrl;
List<String> pathSegments = url.pathSegments();
if (pathSegments.size() < 3) return originUrl;
ListIterator<String> iterator = pathSegments.listIterator(pathSegments.size());
// The last segments, like
// 317a1a254cd9c3269e71b2aa2671fe8d28c91097-260198-640-480-png_250.jpg
if (!iterator.hasPrevious()) return originUrl;
String lastSegment = iterator.previous();
// The second last segments, like
// 7a
if (!iterator.hasPrevious()) return originUrl;
String secondLastSegment = iterator.previous();
// The third last segments, like
// 31
if (!iterator.hasPrevious()) return originUrl;
String thirdLastSegment = iterator.previous();
// Check path segments
if (lastSegment != null && secondLastSegment != null
&& thirdLastSegment != null
&& lastSegment.startsWith(thirdLastSegment)
&& lastSegment.startsWith(secondLastSegment, thirdLastSegment.length())) {
return getThumbUrlPrefix() + thirdLastSegment + "/" + secondLastSegment + "/" + lastSegment;
} else {
return originUrl;
}
}
}
@@ -20,6 +20,7 @@ import android.content.Context;
import android.graphics.Color;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhApplication;
@@ -94,6 +95,7 @@ public class EhUtils {
return CATEGORY_VALUES[i];
}
@NonNull
public static String getCategory(int type) {
int i;
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
@@ -16,16 +16,17 @@
package com.hippo.ehviewer.client.data;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.network.UrlBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FavListUrlBuilder implements Parcelable {
@@ -44,7 +45,9 @@ public class FavListUrlBuilder implements Parcelable {
}
};
private static final String TAG = FavListUrlBuilder.class.getSimpleName();
private int mIndex;
private String mPrev;
private String mNext;
private String mJumpTo;
private String mKeyword;
private int mFavCat = FAV_CAT_ALL;
@@ -52,7 +55,9 @@ public class FavListUrlBuilder implements Parcelable {
}
protected FavListUrlBuilder(Parcel in) {
this.mIndex = in.readInt();
this.mPrev = in.readString();
this.mNext = in.readString();
this.mJumpTo = in.readString();
this.mKeyword = in.readString();
this.mFavCat = in.readInt();
}
@@ -61,8 +66,16 @@ public class FavListUrlBuilder implements Parcelable {
return favCat >= 0 && favCat <= 9;
}
public void setIndex(int index) {
mIndex = index;
public void setPrev(String prev) {
mPrev = prev;
}
public void setNext(String next) {
mNext = next;
}
public void setJumpTo(String jumpTo) {
mJumpTo = jumpTo;
}
public String getKeyword() {
@@ -93,20 +106,30 @@ public class FavListUrlBuilder implements Parcelable {
ub.addQuery("favcat", "all");
}
if (!TextUtils.isEmpty(mKeyword)) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Can't URLEncoder.encode " + mKeyword);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
} else {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8.displayName()));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
}
if (mIndex > 0) {
ub.addQuery("page", Integer.toString(mIndex));
if (mPrev != null && !mPrev.isEmpty()) {
ub.addQuery("prev", mPrev);
}
if (mNext != null && !mNext.isEmpty()) {
ub.addQuery("next", mNext);
}
if (mJumpTo != null && !mJumpTo.isEmpty()) {
ub.addQuery("seek", mJumpTo);
}
return ub.build();
}
@@ -118,7 +141,9 @@ public class FavListUrlBuilder implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mIndex);
dest.writeString(this.mPrev);
dest.writeString(this.mNext);
dest.writeString(this.mJumpTo);
dest.writeString(this.mKeyword);
dest.writeInt(this.mFavCat);
}
@@ -1,636 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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 com.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.widget.AdvanceSearchTable;
import com.hippo.network.UrlBuilder;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class ListUrlBuilder implements Cloneable, Parcelable {
// Mode
public static final int MODE_NORMAL = 0x0;
public static final int MODE_UPLOADER = 0x1;
public static final int MODE_TAG = 0x2;
public static final int MODE_WHATS_HOT = 0x3;
public static final int MODE_IMAGE_SEARCH = 0x4;
public static final int MODE_SUBSCRIPTION = 0x5;
public static final int MODE_TOPLIST = 0x6;
public static final int DEFAULT_ADVANCE = AdvanceSearchTable.SNAME | AdvanceSearchTable.STAGS;
public static final int DEFAULT_MIN_RATING = 2;
public static final Creator<ListUrlBuilder> CREATOR = new Creator<ListUrlBuilder>() {
@Override
public ListUrlBuilder createFromParcel(Parcel source) {
return new ListUrlBuilder(source);
}
@Override
public ListUrlBuilder[] newArray(int size) {
return new ListUrlBuilder[size];
}
};
@Mode
private int mMode = MODE_NORMAL;
private int mPageIndex = 0;
private int mCategory = EhUtils.NONE;
private String mKeyword = null;
private String mSHash = null;
private int mAdvanceSearch = -1;
private int mMinRating = -1;
private int mPageFrom = -1;
private int mPageTo = -1;
private String mImagePath;
private boolean mUseSimilarityScan;
private boolean mOnlySearchCovers;
private boolean mShowExpunged;
public ListUrlBuilder() {
}
@SuppressWarnings("WrongConstant")
protected ListUrlBuilder(Parcel in) {
this.mMode = in.readInt();
this.mPageIndex = in.readInt();
this.mCategory = in.readInt();
this.mKeyword = in.readString();
this.mAdvanceSearch = in.readInt();
this.mMinRating = in.readInt();
this.mPageFrom = in.readInt();
this.mPageTo = in.readInt();
this.mImagePath = in.readString();
this.mUseSimilarityScan = in.readByte() != 0;
this.mOnlySearchCovers = in.readByte() != 0;
this.mShowExpunged = in.readByte() != 0;
this.mSHash = in.readString();
}
/**
* Make this ListUrlBuilder point to homepage
*/
public void reset() {
mMode = MODE_NORMAL;
mPageIndex = 0;
mCategory = EhUtils.NONE;
mKeyword = null;
mAdvanceSearch = -1;
mMinRating = -1;
mPageFrom = -1;
mPageTo = -1;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
mSHash = null;
}
@Override
public ListUrlBuilder clone() {
try {
return (ListUrlBuilder) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
@Mode
public int getMode() {
return mMode;
}
public void setMode(@Mode int mode) {
mMode = mode;
}
public int getPageIndex() {
return mPageIndex;
}
public void setPageIndex(int pageIndex) {
mPageIndex = pageIndex;
}
public int getCategory() {
return mCategory;
}
public void setCategory(int category) {
mCategory = category;
}
public String getKeyword() {
return MODE_UPLOADER == mMode ? "uploader:" + mKeyword : mKeyword;
}
public void setKeyword(String keyword) {
mKeyword = keyword;
}
public int getAdvanceSearch() {
return mAdvanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
mAdvanceSearch = advanceSearch;
}
public int getMinRating() {
return mMinRating;
}
public void setMinRating(int minRating) {
mMinRating = minRating;
}
public int getPageFrom() {
return mPageFrom;
}
public void setPageFrom(int pageFrom) {
mPageFrom = pageFrom;
}
public int getPageTo() {
return mPageTo;
}
public void setPageTo(int pageTo) {
mPageTo = pageTo;
}
@Nullable
public String getImagePath() {
return mImagePath;
}
public void setImagePath(String imagePath) {
mImagePath = imagePath;
}
public boolean isUseSimilarityScan() {
return mUseSimilarityScan;
}
public void setUseSimilarityScan(boolean useSimilarityScan) {
mUseSimilarityScan = useSimilarityScan;
}
public boolean isOnlySearchCovers() {
return mOnlySearchCovers;
}
public void setOnlySearchCovers(boolean onlySearchCovers) {
mOnlySearchCovers = onlySearchCovers;
}
public boolean isShowExpunged() {
return mShowExpunged;
}
public void setShowExpunged(boolean showExpunged) {
mShowExpunged = showExpunged;
}
/**
* Make them the same
*
* @param lub The template
*/
public void set(ListUrlBuilder lub) {
mMode = lub.mMode;
mPageIndex = lub.mPageIndex;
mCategory = lub.mCategory;
mKeyword = lub.mKeyword;
mAdvanceSearch = lub.mAdvanceSearch;
mMinRating = lub.mMinRating;
mPageFrom = lub.mPageFrom;
mPageTo = lub.mPageTo;
mImagePath = lub.mImagePath;
mUseSimilarityScan = lub.mUseSimilarityScan;
mOnlySearchCovers = lub.mOnlySearchCovers;
mShowExpunged = lub.mShowExpunged;
mSHash = lub.mSHash;
}
public void set(QuickSearch q) {
mMode = q.mode;
mCategory = q.category;
mKeyword = q.keyword;
mAdvanceSearch = q.advanceSearch;
mMinRating = q.minRating;
mPageFrom = q.pageFrom;
mPageTo = q.pageTo;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
}
public QuickSearch toQuickSearch() {
QuickSearch q = new QuickSearch();
q.mode = mMode;
q.category = mCategory;
q.keyword = mKeyword;
q.advanceSearch = mAdvanceSearch;
q.minRating = mMinRating;
q.pageFrom = mPageFrom;
q.pageTo = mPageTo;
return q;
}
public boolean equalsQuickSearch(QuickSearch q) {
if (null == q) {
return false;
}
if (q.mode != mMode) {
return false;
}
if (q.category != mCategory) {
return false;
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false;
}
if (q.advanceSearch != mAdvanceSearch) {
return false;
}
if (q.minRating != mMinRating) {
return false;
}
if (q.pageFrom != mPageFrom) {
return false;
}
return q.pageTo == mPageTo;
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
public void setQuery(String query) {
reset();
if (TextUtils.isEmpty(query)) {
return;
}
String[] querys = StringUtils.split(query, '&');
int category = 0;
String keyword = null;
boolean enableAdvanceSearch = false;
int advanceSearch = 0;
boolean enableMinRating = false;
int minRating = -1;
boolean enablePage = false;
int pageFrom = -1;
int pageTo = -1;
for (String str : querys) {
int index = str.indexOf('=');
if (index < 0) {
continue;
}
String key = str.substring(0, index);
String value = str.substring(index + 1);
switch (key) {
case "f_cats":
int cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY);
category |= (~cats) & EhConfig.ALL_CATEGORY;
break;
case "f_doujinshi":
if ("1".equals(value)) {
category |= EhConfig.DOUJINSHI;
}
break;
case "f_manga":
if ("1".equals(value)) {
category |= EhConfig.MANGA;
}
break;
case "f_artistcg":
if ("1".equals(value)) {
category |= EhConfig.ARTIST_CG;
}
break;
case "f_gamecg":
if ("1".equals(value)) {
category |= EhConfig.GAME_CG;
}
break;
case "f_western":
if ("1".equals(value)) {
category |= EhConfig.WESTERN;
}
break;
case "f_non-h":
if ("1".equals(value)) {
category |= EhConfig.NON_H;
}
break;
case "f_imageset":
if ("1".equals(value)) {
category |= EhConfig.IMAGE_SET;
}
break;
case "f_cosplay":
if ("1".equals(value)) {
category |= EhConfig.COSPLAY;
}
break;
case "f_asianporn":
if ("1".equals(value)) {
category |= EhConfig.ASIAN_PORN;
}
break;
case "f_misc":
if ("1".equals(value)) {
category |= EhConfig.MISC;
}
break;
case "f_search":
try {
keyword = URLDecoder.decode(value, "utf-8");
} catch (UnsupportedEncodingException | IllegalArgumentException e) {
// Ignore
}
break;
case "advsearch":
if ("1".equals(value)) {
enableAdvanceSearch = true;
}
break;
case "f_sname":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SNAME;
}
break;
case "f_stags":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STAGS;
}
break;
case "f_sdesc":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDESC;
}
break;
case "f_storr":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STORR;
}
break;
case "f_sto":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STO;
}
break;
case "f_sdt1":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT1;
}
break;
case "f_sdt2":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT2;
}
break;
case "f_sh":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SH;
}
break;
case "f_sfl":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFL;
}
break;
case "f_sfu":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFU;
}
break;
case "f_sft":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFT;
}
break;
case "f_sr":
if ("on".equals(value)) {
enableMinRating = true;
}
break;
case "f_srdd":
minRating = NumberUtils.parseIntSafely(value, -1);
break;
case "f_sp":
if ("on".equals(value)) {
enablePage = true;
}
break;
case "f_spf":
pageFrom = NumberUtils.parseIntSafely(value, -1);
break;
case "f_spt":
pageTo = NumberUtils.parseIntSafely(value, -1);
break;
case "f_shash":
mSHash = value;
break;
}
}
mCategory = category;
mKeyword = keyword;
if (enableAdvanceSearch) {
mAdvanceSearch = advanceSearch;
if (enableMinRating) {
mMinRating = minRating;
} else {
mMinRating = -1;
}
if (enablePage) {
mPageFrom = pageFrom;
mPageTo = pageTo;
} else {
mPageFrom = -1;
mPageTo = -1;
}
} else {
mAdvanceSearch = -1;
}
}
public String build() {
switch (mMode) {
default:
case MODE_NORMAL:
case MODE_SUBSCRIPTION: {
String url;
if (mMode == MODE_NORMAL) {
url = EhUrl.getHost();
} else {
url = EhUrl.getWatchedUrl();
}
UrlBuilder ub = new UrlBuilder(url);
if (mCategory != EhUtils.NONE) {
ub.addQuery("f_cats", (~mCategory) & EhConfig.ALL_CATEGORY);
}
// Search key
if (mKeyword != null) {
String keyword = mKeyword.trim();
if (!keyword.isEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash);
}
// Page index
if (mPageIndex != 0) {
ub.addQuery("page", mPageIndex);
}
// Advance search
if (mAdvanceSearch != -1) {
ub.addQuery("advsearch", "1");
if ((mAdvanceSearch & AdvanceSearchTable.SNAME) != 0)
ub.addQuery("f_sname", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STAGS) != 0)
ub.addQuery("f_stags", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDESC) != 0)
ub.addQuery("f_sdesc", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STORR) != 0)
ub.addQuery("f_storr", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STO) != 0) ub.addQuery("f_sto", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT1) != 0)
ub.addQuery("f_sdt1", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT2) != 0)
ub.addQuery("f_sdt2", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SH) != 0) ub.addQuery("f_sh", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFL) != 0) ub.addQuery("f_sfl", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFU) != 0) ub.addQuery("f_sfu", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFT) != 0) ub.addQuery("f_sft", "on");
// Set min star
if (mMinRating != -1) {
ub.addQuery("f_sr", "on");
ub.addQuery("f_srdd", mMinRating);
}
// Pages
if (mPageFrom != -1 || mPageTo != -1) {
ub.addQuery("f_sp", "on");
ub.addQuery("f_spf", mPageFrom != -1 ? Integer.toString(mPageFrom) : "");
ub.addQuery("f_spt", mPageTo != -1 ? Integer.toString(mPageTo) : "");
}
}
return ub.build();
}
case MODE_UPLOADER: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("uploader/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
return sb.toString();
}
case MODE_TAG: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("tag/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
return sb.toString();
}
case MODE_WHATS_HOT:
return EhUrl.getPopularUrl();
case MODE_IMAGE_SEARCH:
return EhUrl.getImageSearchUrl();
case MODE_TOPLIST:
StringBuilder sb = new StringBuilder(EhUrl.HOST_E);
sb.append("toplist.php?tl=");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append("&p=").append(mPageIndex);
}
return sb.toString();
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode);
dest.writeInt(this.mPageIndex);
dest.writeInt(this.mCategory);
dest.writeString(this.mKeyword);
dest.writeInt(this.mAdvanceSearch);
dest.writeInt(this.mMinRating);
dest.writeInt(this.mPageFrom);
dest.writeInt(this.mPageTo);
dest.writeString(this.mImagePath);
dest.writeByte(mUseSimilarityScan ? (byte) 1 : (byte) 0);
dest.writeByte(mOnlySearchCovers ? (byte) 1 : (byte) 0);
dest.writeByte(mShowExpunged ? (byte) 1 : (byte) 0);
dest.writeString(this.mSHash);
}
@IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH, MODE_SUBSCRIPTION, MODE_TOPLIST})
@Retention(RetentionPolicy.SOURCE)
private @interface Mode {
}
}
@@ -0,0 +1,541 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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 com.hippo.ehviewer.client.data
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhConfig
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.widget.AdvanceSearchTable
import com.hippo.network.UrlBuilder
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
open class ListUrlBuilder : Cloneable, Parcelable {
@get:Mode
@Mode
var mode = MODE_NORMAL
private var mPrevGid = 0
private var mNextGid = 0
private var mJumpTo: String? = null
var category = EhUtils.NONE
private var mKeyword: String? = null
private var mSHash: String? = null
var advanceSearch = -1
var minRating = -1
var pageFrom = -1
var pageTo = -1
var imagePath: String? = null
var isUseSimilarityScan = false
var isOnlySearchCovers = false
var isShowExpunged = false
constructor()
protected constructor(parcel: Parcel) {
mode = parcel.readInt()
mPrevGid = parcel.readInt()
mNextGid = parcel.readInt()
mJumpTo = parcel.readString()
this.category = parcel.readInt()
mKeyword = parcel.readString()
advanceSearch = parcel.readInt()
minRating = parcel.readInt()
pageFrom = parcel.readInt()
pageTo = parcel.readInt()
imagePath = parcel.readString()
isUseSimilarityScan = parcel.readByte().toInt() != 0
isOnlySearchCovers = parcel.readByte().toInt() != 0
isShowExpunged = parcel.readByte().toInt() != 0
mSHash = parcel.readString()
}
fun reset() {
mode = MODE_NORMAL
mPrevGid = 0
mNextGid = 0
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
mSHash = null
}
public override fun clone(): ListUrlBuilder {
return try {
super.clone() as ListUrlBuilder
} catch (e: CloneNotSupportedException) {
throw IllegalStateException(e)
}
}
fun setPrevGid(prevGid: Int) {
mPrevGid = prevGid
}
fun setNextGid(nextGid: Int) {
mNextGid = nextGid
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
/**
* Make them the same
*
* @param lub The template
*/
fun set(lub: ListUrlBuilder) {
mode = lub.mode
mPrevGid = lub.mPrevGid
mNextGid = lub.mNextGid
mJumpTo = lub.mJumpTo
this.category = lub.category
mKeyword = lub.mKeyword
advanceSearch = lub.advanceSearch
minRating = lub.minRating
pageFrom = lub.pageFrom
pageTo = lub.pageTo
imagePath = lub.imagePath
isUseSimilarityScan = lub.isUseSimilarityScan
isOnlySearchCovers = lub.isOnlySearchCovers
isShowExpunged = lub.isShowExpunged
mSHash = lub.mSHash
}
fun set(q: QuickSearch) {
mode = q.mode
this.category = q.category
mKeyword = q.keyword
advanceSearch = q.advanceSearch
minRating = q.minRating
pageFrom = q.pageFrom
pageTo = q.pageTo
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
}
fun toQuickSearch(): QuickSearch {
val q = QuickSearch()
q.mode = mode
q.category = this.category
q.keyword = mKeyword
q.advanceSearch = advanceSearch
q.minRating = minRating
q.pageFrom = pageFrom
q.pageTo = pageTo
return q
}
fun equalsQuickSearch(q: QuickSearch?): Boolean {
if (null == q) {
return false
}
if (q.mode != mode) {
return false
}
if (q.category != this.category) {
return false
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false
}
if (q.advanceSearch != advanceSearch) {
return false
}
if (q.minRating != minRating) {
return false
}
return if (q.pageFrom != pageFrom) {
false
} else q.pageTo == pageTo
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
fun setQuery(query: String?) {
reset()
if (TextUtils.isEmpty(query)) {
return
}
val querys = StringUtils.split(query, '&')
var category = 0
var keyword: String? = null
var enableAdvanceSearch = false
var advanceSearch = 0
var enableMinRating = false
var minRating = -1
var enablePage = false
var pageFrom = -1
var pageTo = -1
for (str in querys) {
val index = str.indexOf('=')
if (index < 0) {
continue
}
val key = str.substring(0, index)
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY)
category = category or (cats.inv() and EhConfig.ALL_CATEGORY)
}
"f_doujinshi" -> if ("1" == value) {
category = category or EhConfig.DOUJINSHI
}
"f_manga" -> if ("1" == value) {
category = category or EhConfig.MANGA
}
"f_artistcg" -> if ("1" == value) {
category = category or EhConfig.ARTIST_CG
}
"f_gamecg" -> if ("1" == value) {
category = category or EhConfig.GAME_CG
}
"f_western" -> if ("1" == value) {
category = category or EhConfig.WESTERN
}
"f_non-h" -> if ("1" == value) {
category = category or EhConfig.NON_H
}
"f_imageset" -> if ("1" == value) {
category = category or EhConfig.IMAGE_SET
}
"f_cosplay" -> if ("1" == value) {
category = category or EhConfig.COSPLAY
}
"f_asianporn" -> if ("1" == value) {
category = category or EhConfig.ASIAN_PORN
}
"f_misc" -> if ("1" == value) {
category = category or EhConfig.MISC
}
"f_search" -> try {
keyword = URLDecoder.decode(value, "utf-8")
} catch (e: UnsupportedEncodingException) {
// Ignore
} catch (_: IllegalArgumentException) {
}
"advsearch" -> if ("1" == value) {
enableAdvanceSearch = true
}
"f_sh" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SH
}
"f_sto" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.STO
}
"f_sfl" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFL
}
"f_sfu" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFU
}
"f_sft" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFT
}
"f_sr" -> if ("on" == value) {
enableMinRating = true
}
"f_srdd" -> minRating = NumberUtils.parseIntSafely(value, -1)
"f_sp" -> if ("on" == value) {
enablePage = true
}
"f_spf" -> pageFrom = NumberUtils.parseIntSafely(value, -1)
"f_spt" -> pageTo = NumberUtils.parseIntSafely(value, -1)
"f_shash" -> mSHash = value
}
}
this.category = category
mKeyword = keyword
if (enableAdvanceSearch) {
this.advanceSearch = advanceSearch
if (enableMinRating) {
this.minRating = minRating
} else {
this.minRating = -1
}
if (enablePage) {
this.pageFrom = pageFrom
this.pageTo = pageTo
} else {
this.pageFrom = -1
this.pageTo = -1
}
} else {
this.advanceSearch = -1
}
}
fun build(): String {
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
// Search key
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
mJumpTo?.let {
ub.addQuery("seek", mJumpTo!!)
mPrevGid = 0
mNextGid = 0
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
// Set min star
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
// Pages
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("uploader/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
sb.toString()
}
MODE_TAG -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("tag/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
MODE_TOPLIST -> {
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mNextGid != 0) {
sb.append("&p=").append(mNextGid)
}
sb.toString()
}
else -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (_: UnsupportedEncodingException) {
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
}
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(mode)
dest.writeInt(mPrevGid)
dest.writeInt(mNextGid)
dest.writeString(mJumpTo)
dest.writeInt(this.category)
dest.writeString(mKeyword)
dest.writeInt(advanceSearch)
dest.writeInt(minRating)
dest.writeInt(pageFrom)
dest.writeInt(pageTo)
dest.writeString(imagePath)
dest.writeByte(if (isUseSimilarityScan) 1.toByte() else 0.toByte())
dest.writeByte(if (isOnlySearchCovers) 1.toByte() else 0.toByte())
dest.writeByte(if (isShowExpunged) 1.toByte() else 0.toByte())
dest.writeString(mSHash)
}
@IntDef(
MODE_NORMAL,
MODE_UPLOADER,
MODE_TAG,
MODE_WHATS_HOT,
MODE_IMAGE_SEARCH,
MODE_SUBSCRIPTION,
MODE_TOPLIST
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Mode
companion object {
const val MODE_NORMAL = 0x0
const val MODE_UPLOADER = 0x1
const val MODE_TAG = 0x2
const val MODE_WHATS_HOT = 0x3
const val MODE_IMAGE_SEARCH = 0x4
const val MODE_SUBSCRIPTION = 0x5
const val MODE_TOPLIST = 0x6
@JvmField
val CREATOR: Creator<ListUrlBuilder> = object : Creator<ListUrlBuilder> {
override fun createFromParcel(source: Parcel): ListUrlBuilder {
return ListUrlBuilder(source)
}
override fun newArray(size: Int): Array<ListUrlBuilder?> {
return arrayOfNulls(size)
}
}
}
}
@@ -31,18 +31,22 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FavoritesParser {
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+(-\\d+)?)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+(-\\d+)?)");
public static Result parse(String body) throws Exception {
if (body.contains("This page requires you to log on.</p>")) {
throw new EhException(GetText.getString(R.string.need_sign_in));
}
Document d = Jsoup.parse(body);
String[] catArray = new String[10];
int[] countArray = new int[10];
try {
Document d = Jsoup.parse(body);
Element ido = JsoupUtils.getElementByClass(d, "ido");
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
@@ -60,13 +64,24 @@ public class FavoritesParser {
throw new ParseException("Parse favorites error", body);
}
Result re = new Result();
try {
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find()) re.prevPage = matcherPrev.group(1);
if (matcherNext.find()) re.nextPage = matcherNext.group(1);
} catch (Throwable e) {
e.printStackTrace();
}
GalleryListParser.Result result = GalleryListParser.parse(body);
Result re = new Result();
re.catArray = catArray;
re.countArray = countArray;
re.pages = result.pages;
re.nextPage = result.nextPage;
re.galleryInfoList = result.galleryInfoList;
return re;
@@ -75,8 +90,8 @@ public class FavoritesParser {
public static class Result {
public String[] catArray; // Size 10
public int[] countArray; // Size 10
public int pages;
public int nextPage;
public String prevPage;
public String nextPage;
public List<GalleryInfo> galleryInfoList;
}
}
@@ -19,8 +19,6 @@ package com.hippo.ehviewer.client.parser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
@@ -646,9 +644,6 @@ public class GalleryDetailParser {
String pageUrl = element.attr("href");
element = element.child(0);
String imageUrl = element.attr("src");
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
int index = Integer.parseInt(element.attr("alt")) - 1;
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -674,9 +669,6 @@ public class GalleryDetailParser {
}
String imageUrl = ParserUtils.trim(m.group(3));
String pageUrl = ParserUtils.trim(m.group(1));
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -51,7 +51,8 @@ public class GalleryListParser {
private static final Pattern PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px;width:(\\d+)px");
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("background-color:rgba\\((\\d+),(\\d+),(\\d+),");
private static final Pattern PATTERN_PAGES = Pattern.compile("(\\d+) page");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("page=(\\d+)");
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+)");
private static final String[][] FAVORITE_SLOT_RGB = new String[][]{
new String[]{"0", "0", "0"},
@@ -298,37 +299,33 @@ public class GalleryListParser {
}
public static Result parse(@NonNull String body) throws Exception {
Result result = new Result();
Document d = Jsoup.parse(body);
try {
Element ptt = d.getElementsByClass("ptt").first();
Elements es = ptt.child(0).child(0).children();
result.pages = Integer.parseInt(es.get(es.size() - 2).text().trim());
Element e = es.get(es.size() - 1);
if (e != null) {
e = e.children().first();
if (e != null) {
String href = e.attr("href");
Matcher matcher = PATTERN_NEXT_PAGE.matcher(href);
if (matcher.find()) {
result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0);
}
}
}
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find())
result.prevGid = NumberUtils.parseIntSafely(matcherPrev.group(1), 0);
if (matcherNext.find())
result.nextGid = NumberUtils.parseIntSafely(matcherNext.group(1), 0);
result.founds = Integer.MAX_VALUE;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
result.noWatchedTags = body.contains("<p>You do not have any watched tags");
if (body.contains("No hits found</p>")) {
result.pages = 0;
result.founds = 0;
//noinspection unchecked
result.galleryInfoList = Collections.EMPTY_LIST;
return result;
} else if (d.getElementsByClass("ptt").isEmpty()) {
result.pages = 1;
result.founds = 1;
} else {
result.pages = Integer.MAX_VALUE;
result.founds = Integer.MAX_VALUE;
}
}
@@ -350,7 +347,7 @@ public class GalleryListParser {
}
if (list.isEmpty()) {
if (es.size() < 2 || !NO_UNFILTERED_TEXT.equals(es.get(1).text())) {
throw new ParseException("No gallery", body);
Log.d(TAG, "No gallery found");
}
}
result.galleryInfoList = list;
@@ -364,8 +361,9 @@ public class GalleryListParser {
}
public static class Result {
public int pages;
public int nextPage;
public int founds;
public int prevGid;
public int nextGid;
public boolean noWatchedTags;
public List<GalleryInfo> galleryInfoList;
}
@@ -17,6 +17,7 @@
package com.hippo.ehviewer.client.parser;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import com.hippo.ehviewer.client.EhUrl;
@@ -27,6 +28,7 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public final class GalleryListUrlParser {
@@ -91,10 +93,15 @@ public final class GalleryListUrlParser {
uploader = path.substring(prefixLength, index);
}
try {
uploader = URLDecoder.decode(uploader, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8);
} else {
try {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(uploader)) {
@@ -120,10 +127,15 @@ public final class GalleryListUrlParser {
tag = path.substring(prefixLength, index);
}
try {
tag = URLDecoder.decode(tag, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8);
} else {
try {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(tag)) {
@@ -4,5 +4,6 @@ import java.util.List;
public interface BasicDao<T> {
List<T> list();
long insert(T t);
}
@@ -58,10 +58,9 @@ public class Filter {
@Override
public boolean equals(Object o) {
if (!(o instanceof Filter)) {
if (!(o instanceof Filter filter)) {
return false;
}
Filter filter = (Filter) o;
return filter.mode == mode && ObjectUtils.equal(filter.text, text);
}
}
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.dao;
import androidx.paging.PagingSource;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
@@ -19,6 +20,9 @@ public interface HistoryDao extends BasicDao<HistoryInfo> {
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC LIMIT :limit OFFSET :offset")
List<HistoryInfo> list(int offset, int limit);
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
PagingSource<Integer, HistoryInfo> listLazy();
@Update
void update(HistoryInfo historyInfo);
@@ -1,581 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.scene.DownloadsScene;
import com.hippo.scene.StageActivity;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.collect.LongList;
import com.hippo.yorozuya.collect.SparseJBArray;
import com.hippo.yorozuya.collect.SparseJLArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class DownloadService extends Service implements DownloadManager.DownloadListener {
public static final String ACTION_START = "start";
public static final String ACTION_START_RANGE = "start_range";
public static final String ACTION_START_ALL = "start_all";
public static final String ACTION_STOP = "stop";
public static final String ACTION_STOP_RANGE = "stop_range";
public static final String ACTION_STOP_CURRENT = "stop_current";
public static final String ACTION_STOP_ALL = "stop_all";
public static final String ACTION_DELETE = "delete";
public static final String ACTION_DELETE_RANGE = "delete_range";
public static final String ACTION_CLEAR = "clear";
public static final String KEY_GALLERY_INFO = "gallery_info";
public static final String KEY_LABEL = "label";
public static final String KEY_GID = "gid";
public static final String KEY_GID_LIST = "gid_list";
private static final int ID_DOWNLOADING = 1;
private static final int ID_DOWNLOADED = 2;
private static final int ID_509 = 3;
private final static SparseJBArray sItemStateArray = new SparseJBArray();
private final static SparseJLArray<String> sItemTitleArray = new SparseJLArray<>();
private static int sFailedCount;
private static int sFinishedCount;
private static int sDownloadedCount;
@Nullable
private NotificationManagerCompat mNotifyManager;
@Nullable
private DownloadManager mDownloadManager;
private NotificationCompat.Builder mDownloadingBuilder;
private NotificationCompat.Builder mDownloadedBuilder;
private NotificationCompat.Builder m509dBuilder;
private NotificationDelay mDownloadingDelay;
private NotificationDelay mDownloadedDelay;
private NotificationDelay m509Delay;
private String CHANNEL_ID;
public static void clear() {
sFailedCount = 0;
sFinishedCount = 0;
sDownloadedCount = 0;
sItemStateArray.clear();
sItemTitleArray.clear();
}
@Override
public void onCreate() {
super.onCreate();
CHANNEL_ID = getPackageName() + ".download";
mNotifyManager = NotificationManagerCompat.from(this);
mNotifyManager.createNotificationChannel(
new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.download_service))
.build());
mDownloadManager = EhApplication.getDownloadManager(getApplicationContext());
mDownloadManager.setDownloadListener(this);
ensureDownloadingBuilder();
mDownloadingBuilder.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true);
startForeground(ID_DOWNLOADING, mDownloadingBuilder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
mNotifyManager = null;
if (mDownloadManager != null) {
mDownloadManager.setDownloadListener(null);
mDownloadManager = null;
}
mDownloadingBuilder = null;
mDownloadedBuilder = null;
m509dBuilder = null;
if (mDownloadingDelay != null) {
mDownloadingDelay.release();
}
if (mDownloadedDelay != null) {
mDownloadedDelay.release();
}
if (m509Delay != null) {
m509Delay.release();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return START_STICKY;
}
private void handleIntent(Intent intent) {
String action = null;
if (intent != null) {
action = intent.getAction();
}
if (ACTION_START.equals(action)) {
GalleryInfo gi = intent.getParcelableExtra(KEY_GALLERY_INFO);
String label = intent.getStringExtra(KEY_LABEL);
if (gi != null && mDownloadManager != null) {
mDownloadManager.startDownload(gi, label);
}
} else if (ACTION_START_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.startRangeDownload(gidList);
}
} else if (ACTION_START_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.startAllDownload();
}
} else if (ACTION_STOP.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.stopDownload(gid);
}
} else if (ACTION_STOP_CURRENT.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopCurrentDownload();
}
} else if (ACTION_STOP_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.stopRangeDownload(gidList);
}
} else if (ACTION_STOP_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
} else if (ACTION_DELETE.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.deleteDownload(gid);
}
} else if (ACTION_DELETE_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.deleteRangeDownload(gidList);
}
} else if (ACTION_CLEAR.equals(action)) {
clear();
}
checkStopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new IllegalStateException("No bindService");
}
private void ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return;
}
Intent stopAllIntent = new Intent(this, DownloadService.class);
stopAllIntent.setAction(ACTION_STOP_ALL);
PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
.setShowWhen(false)
.setChannelId(CHANNEL_ID);
mDownloadingDelay = new NotificationDelay(this, mNotifyManager, mDownloadingBuilder, ID_DOWNLOADING);
}
private void ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return;
}
Intent clearIntent = new Intent(this, DownloadService.class);
clearIntent.setAction(ACTION_CLEAR);
PendingIntent piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE);
Bundle bundle = new Bundle();
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity);
mDownloadedDelay = new NotificationDelay(this, mNotifyManager, mDownloadedBuilder, ID_DOWNLOADED);
}
private void ensure509Builder() {
if (m509dBuilder != null) {
return;
}
m509dBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)))
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR);
m509Delay = new NotificationDelay(this, mNotifyManager, m509dBuilder, ID_509);
}
@Override
public void onGet509() {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
if (mNotifyManager == null) {
return;
}
ensure509Builder();
m509dBuilder.setWhen(System.currentTimeMillis());
m509Delay.show();
}
@Override
public void onStart(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
Bundle bundle = new Bundle();
bundle.putLong(DownloadsScene.KEY_GID, info.gid);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity);
mDownloadingDelay.startForeground();
}
private void onUpdate(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
long speed = info.speed;
if (speed < 0) {
speed = 0;
}
String text = FileUtils.humanReadableByteCount(speed, false) + "/s";
long remaining = info.remaining;
if (remaining >= 0) {
text = getString(R.string.download_speed_text_2, text, ReadableTime.getShortTimeInterval(remaining));
} else {
text = getString(R.string.download_speed_text, text);
}
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
.setSubText(info.total == -1 || info.finished == -1 ? null : info.finished + "/" + info.total)
.setProgress(info.total, info.finished, false);
mDownloadingDelay.startForeground();
}
@Override
public void onDownload(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onGetPage(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onFinish(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
ensureDownloadedBuilder();
boolean finish = info.state == DownloadInfo.STATE_FINISH;
long gid = info.gid;
int index = sItemStateArray.indexOfKey(gid);
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
sDownloadedCount++;
if (finish) {
sFinishedCount++;
} else {
sFailedCount++;
}
} else { // Contain
boolean oldFinish = sItemStateArray.valueAt(index);
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
if (oldFinish && !finish) {
sFinishedCount--;
sFailedCount++;
} else if (!oldFinish && finish) {
sFinishedCount++;
sFailedCount--;
}
}
String text;
boolean needStyle;
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_succeeded, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount);
needStyle = true;
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_failed, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount);
needStyle = true;
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount);
needStyle = true;
}
NotificationCompat.InboxStyle style;
if (needStyle) {
style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(getString(R.string.stat_download_done_title));
SparseJBArray stateArray = sItemStateArray;
for (int i = 0, n = stateArray.size(); i < n; i++) {
long id = stateArray.keyAt(i);
boolean fin = stateArray.valueAt(i);
String title = sItemTitleArray.get(id);
if (title == null) {
continue;
}
style.addLine(getString(fin ? R.string.stat_download_done_line_succeeded :
R.string.stat_download_done_line_failed, title));
}
} else {
style = null;
}
mDownloadedBuilder.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount);
mDownloadedDelay.show();
checkStopSelf();
}
@Override
public void onCancel(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
checkStopSelf();
}
private void checkStopSelf() {
if (mDownloadManager == null || mDownloadManager.isIdle()) {
stopForeground(true);
stopSelf();
}
}
// TODO Include all notification in one delay
// Avoid frequent notification
private static class NotificationDelay implements Runnable {
private static final int OPS_NOTIFY = 0;
private static final int OPS_CANCEL = 1;
private static final int OPS_START_FOREGROUND = 2;
private static final long DELAY = 1000; // 1s
private final NotificationManagerCompat mNotifyManager;
private final NotificationCompat.Builder mBuilder;
private final int mId;
private Service mService;
private long mLastTime;
private boolean mPosted;
// false for show, true for cancel
@Ops
private int mOps;
public NotificationDelay(Service service, NotificationManagerCompat notifyManager,
NotificationCompat.Builder builder, int id) {
mService = service;
mNotifyManager = notifyManager;
mBuilder = builder;
mId = id;
}
public void release() {
mService = null;
}
public void show() {
if (mPosted) {
mOps = OPS_NOTIFY;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.notify(mId, mBuilder.build());
} else {
// Too quick, post delay
mOps = OPS_NOTIFY;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
mLastTime = now;
}
}
public void cancel() {
if (mPosted) {
mOps = OPS_CANCEL;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId);
} else {
// Too quick, post delay
mOps = OPS_CANCEL;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
public void startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
@Override
public void run() {
mPosted = false;
switch (mOps) {
case OPS_NOTIFY:
mNotifyManager.notify(mId, mBuilder.build());
break;
case OPS_CANCEL:
mNotifyManager.cancel(mId);
break;
case OPS_START_FOREGROUND:
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
break;
}
}
@IntDef({OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND})
@Retention(RetentionPolicy.SOURCE)
private @interface Ops {
}
}
}
@@ -0,0 +1,551 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.download
import android.Manifest
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.annotation.IntDef
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.scene.StageActivity
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.collect.LongList
import com.hippo.yorozuya.collect.SparseJBArray
import com.hippo.yorozuya.collect.SparseJLArray
class DownloadService : Service(), DownloadManager.DownloadListener {
private var mNotifyManager: NotificationManagerCompat? = null
private var mDownloadManager: DownloadManager? = null
private var mDownloadingBuilder: NotificationCompat.Builder? = null
private var mDownloadedBuilder: NotificationCompat.Builder? = null
private var m509dBuilder: NotificationCompat.Builder? = null
private var mDownloadingDelay: NotificationDelay? = null
private var mDownloadedDelay: NotificationDelay? = null
private var m509Delay: NotificationDelay? = null
private var mChannelID: String? = null
override fun onCreate() {
super.onCreate()
mChannelID = "$packageName.download"
mNotifyManager = NotificationManagerCompat.from(this)
mNotifyManager!!.createNotificationChannel(
NotificationChannelCompat.Builder(
mChannelID!!,
NotificationManagerCompat.IMPORTANCE_LOW
)
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.getDownloadManager(applicationContext)
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())
}
override fun onDestroy() {
super.onDestroy()
mNotifyManager = null
if (mDownloadManager != null) {
mDownloadManager!!.setDownloadListener(null)
mDownloadManager = null
}
mDownloadingBuilder = null
mDownloadedBuilder = null
m509dBuilder = null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
handleIntent(intent)
return START_STICKY
}
private fun handleIntent(intent: Intent?) {
var action: String? = null
if (intent != null) {
action = intent.action
}
if (ACTION_START == action) {
val gi = intent!!.getParcelableExtra<GalleryInfo>(KEY_GALLERY_INFO)
val label = intent.getStringExtra(KEY_LABEL)
if (gi != null && mDownloadManager != null) {
mDownloadManager!!.startDownload(gi, label)
}
} else if (ACTION_START_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.startRangeDownload(gidList)
}
} else if (ACTION_START_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.startAllDownload()
}
} else if (ACTION_STOP == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.stopDownload(gid)
}
} else if (ACTION_STOP_CURRENT == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopCurrentDownload()
}
} else if (ACTION_STOP_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.stopRangeDownload(gidList)
}
} else if (ACTION_STOP_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
} else if (ACTION_DELETE == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.deleteDownload(gid)
}
} else if (ACTION_DELETE_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.deleteRangeDownload(gidList)
}
} else if (ACTION_CLEAR == action) {
clear()
}
checkStopSelf()
}
override fun onBind(intent: Intent): IBinder? {
throw IllegalStateException("No bindService")
}
private fun ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return
}
val stopAllIntent = Intent(this, DownloadService::class.java)
stopAllIntent.action = ACTION_STOP_ALL
val piStopAll =
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
R.drawable.v_pause_x24,
getString(R.string.stat_download_action_stop_all),
piStopAll
)
.setShowWhen(false)
.setChannelId(mChannelID!!)
mDownloadingDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)
}
private fun ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return
}
val clearIntent = Intent(this, DownloadService::class.java)
clearIntent.action = ACTION_CLEAR
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
val bundle = Bundle()
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity)
mDownloadedDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)
}
private fun ensure509Builder() {
if (m509dBuilder != null) {
return
}
m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text))
)
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR)
m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)
}
override fun onGet509() {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
if (mNotifyManager == null) {
return
}
ensure509Builder()
m509dBuilder!!.setWhen(System.currentTimeMillis())
m509Delay!!.show()
}
override fun onStart(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
val bundle = Bundle()
bundle.putLong(DownloadsScene.KEY_GID, info.gid)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity)
mDownloadingDelay!!.startForeground()
}
private fun onUpdate(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
var speed = info.speed
if (speed < 0) {
speed = 0
}
var text = FileUtils.humanReadableByteCount(speed, false) + "/s"
val remaining = info.remaining
text = if (remaining >= 0) {
getString(
R.string.download_speed_text_2,
text,
ReadableTime.getShortTimeInterval(remaining)
)
} else {
getString(R.string.download_speed_text, text)
}
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
.setProgress(info.total, info.finished, false)
mDownloadingDelay!!.startForeground()
}
override fun onDownload(info: DownloadInfo) {
onUpdate(info)
}
override fun onGetPage(info: DownloadInfo) {
onUpdate(info)
}
override fun onFinish(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
ensureDownloadedBuilder()
val finish = info.state == DownloadInfo.STATE_FINISH
val gid = info.gid
val index = sItemStateArray.indexOfKey(gid)
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
sDownloadedCount++
if (finish) {
sFinishedCount++
} else {
sFailedCount++
}
} else { // Contain
val oldFinish = sItemStateArray.valueAt(index)
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
if (oldFinish && !finish) {
sFinishedCount--
sFailedCount++
} else if (!oldFinish && finish) {
sFinishedCount++
sFailedCount--
}
}
val text: String
val needStyle: Boolean
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_succeeded,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount)
needStyle = true
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_failed,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount)
needStyle = true
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount)
needStyle = true
}
val style: NotificationCompat.InboxStyle?
if (needStyle) {
style = NotificationCompat.InboxStyle()
style.setBigContentTitle(getString(R.string.stat_download_done_title))
val stateArray = sItemStateArray
var i = 0
val n = stateArray.size()
while (i < n) {
val id = stateArray.keyAt(i)
val fin = stateArray.valueAt(i)
val title = sItemTitleArray[id]
if (title == null) {
i++
continue
}
style.addLine(
getString(
if (fin) R.string.stat_download_done_line_succeeded else R.string.stat_download_done_line_failed,
title
)
)
i++
}
} else {
style = null
}
mDownloadedBuilder!!.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount)
mDownloadedDelay!!.show()
checkStopSelf()
}
override fun onCancel(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
checkStopSelf()
}
private fun checkStopSelf() {
if (mDownloadManager == null || mDownloadManager!!.isIdle) {
stopForeground(true)
stopSelf()
}
}
private class NotificationDelay(
private var mService: Service,
private val mNotifyManager: NotificationManagerCompat,
private val mBuilder: NotificationCompat.Builder,
private val mId: Int
) : Runnable {
private var mLastTime: Long = 0
private var mPosted = false
// false for show, true for cancel
@Ops
private var mOps = 0
fun show() {
if (mPosted) {
mOps = OPS_NOTIFY
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_NOTIFY
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
mLastTime = now
}
}
fun cancel() {
if (mPosted) {
mOps = OPS_CANCEL
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId)
} else {
// Too quick, post delay
mOps = OPS_CANCEL
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
fun startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mService.startForeground(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
override fun run() {
mPosted = false
when (mOps) {
OPS_NOTIFY -> {
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
}
OPS_CANCEL -> mNotifyManager.cancel(mId)
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
}
}
@IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)
@Retention(AnnotationRetention.SOURCE)
private annotation class Ops
companion object {
private const val OPS_NOTIFY = 0
private const val OPS_CANCEL = 1
private const val OPS_START_FOREGROUND = 2
private const val DELAY: Long = 1000 // 1s
}
}
companion object {
const val ACTION_START = "start"
const val ACTION_START_RANGE = "start_range"
const val ACTION_START_ALL = "start_all"
const val ACTION_STOP = "stop"
const val ACTION_STOP_RANGE = "stop_range"
const val ACTION_STOP_CURRENT = "stop_current"
const val ACTION_STOP_ALL = "stop_all"
const val ACTION_DELETE = "delete"
const val ACTION_DELETE_RANGE = "delete_range"
const val ACTION_CLEAR = "clear"
const val KEY_GALLERY_INFO = "gallery_info"
const val KEY_LABEL = "label"
const val KEY_GID = "gid"
const val KEY_GID_LIST = "gid_list"
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private val sItemStateArray = SparseJBArray()
private val sItemTitleArray = SparseJLArray<String>()
private var sFailedCount = 0
private var sFinishedCount = 0
private var sDownloadedCount = 0
@JvmStatic
fun clear() {
sFailedCount = 0
sFinishedCount = 0
sDownloadedCount = 0
sItemStateArray.clear()
sItemTitleArray.clear()
}
}
}
@@ -19,21 +19,27 @@ package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.Native;
import com.hippo.UriArchiveAccessor;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.glgallery.GalleryPageView;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -48,11 +54,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private final LinkedHashMap<Integer, Long> streams = new LinkedHashMap<>();
private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private final LinkedHashMap<Integer, ByteBuffer> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private Thread archiveThread;
private Thread decodeThread;
private volatile int size = STATE_WAIT;
private String error;
@@ -74,25 +83,22 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
int id = sIdGenerator.incrementAndGet();
archiveThread = new PriorityThread(
new ArchiveTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
archiveThread.start();
decodeThread = new PriorityThread(
new DecodeTask(), "DecodeTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
decodeThread.start();
for (Thread i : decodeThread) {
i.start();
}
}
@Override
public void stop() {
super.stop();
if (archiveThread != null) {
archiveThread.interrupt();
archiveThread = null;
}
if (decodeThread != null) {
decodeThread.interrupt();
decodeThread = null;
for (Thread i : decodeThread) {
i.interrupt();
}
try {
archiveAccessor.close();
@@ -110,11 +116,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
protected void onRequest(int index) {
boolean inDecodeTask;
synchronized (streams) {
inDecodeTask = streams.containsKey(index) || index == decodingIndex.get();
inDecodeTask = streams.containsKey(index);
}
synchronized (requests) {
boolean inArchiveTask = requests.contains(index) || index == extractingIndex.get();
boolean inArchiveTask = requests.contains(index);
if (!inArchiveTask && !inDecodeTask) {
requests.add(index);
requests.notify();
@@ -143,31 +149,53 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
@NonNull
@Override
public String getImageFilename(int index) {
// TODO
return Integer.toString(index);
}
@Override
public boolean save(int index, @NonNull UniFile file) {
// TODO
return false;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
// TODO
return null;
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
// TODO
return Integer.toString(index);
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
}
private class ArchiveTask implements Runnable {
@Override
public boolean save(int index, @NonNull UniFile file) {
int fd;
FileOutputStream stream;
try {
stream = (FileOutputStream) file.openOutputStream();
fd = Native.getFd(stream.getFD());
} catch (IOException e) {
e.printStackTrace();
return false;
}
archiveAccessor.extractToFd(index, fd);
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
save(index, dst);
return dst;
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask())
};
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
@@ -225,7 +253,26 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
}
for (Thread i : archiveThreads) {
i.start();
}
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
for (Thread i : archiveThreads) {
i.interrupt();
}
}
}
}
}
private class ArchiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
@@ -239,12 +286,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
continue;
}
index = requests.pop();
extractingIndex.lazySet(index);
}
// Check index valid
if (index < 0 || index >= size) {
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
continue;
}
@@ -255,10 +300,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
long addr = archiveAccessor.extracttoOutputStream(index);
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
synchronized (streams) {
streams.put(index, addr);
streams.put(index, buffer);
streams.notify();
}
}
@@ -270,35 +315,35 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Long addr;
ByteBuffer buffer;
synchronized (streams) {
if (streams.isEmpty()) {
try {
streams.wait();
} catch (InterruptedException e) {
// Interrupted
break;
}
continue;
}
Iterator<Map.Entry<Integer, Long>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Long> entry = iterator.next();
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, ByteBuffer> entry = iterator.next();
iterator.remove();
index = entry.getKey();
addr = entry.getValue();
decodingIndex.lazySet(index);
buffer = entry.getValue();
}
try {
Image image = Image.decodeAddr(addr, true);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
} finally {
decodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
Image image = null;
if (buffer != null) {
image = Image.decode(buffer, false, () -> {
archiveAccessor.releaseByteBuffer(buffer);
return null;
});
}
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
}
}
@@ -259,7 +259,7 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable {
InputStream is = null;
try {
is = files[index].openInputStream();
Image image = Image.decode(((FileInputStream) is).getFD(), true);
Image image = Image.decode((FileInputStream) is, false);
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
if (image != null) {
notifyPageSucceed(index, image);
@@ -0,0 +1,93 @@
/*
* Copyright 2018 Hippo Seven
*
* 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 com.hippo.ehviewer.preference
import android.content.Context
import android.content.DialogInterface
import android.text.Html
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.preference.MessagePreference
import com.hippo.util.addTextToClipboard
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.LinkedList
class AccountPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MessagePreference(context, attrs) {
private val mActivity = context as SettingsActivity
private var message: String? = null
init {
val store = EhApplication.getEhCookieStore(context)
val eCookies = store.getCookies(EhUrl.HOST_E.toHttpUrl())
val exCookies = store.getCookies(EhUrl.HOST_EX.toHttpUrl())
val cookies: MutableList<Cookie> = LinkedList(eCookies)
cookies.addAll(exCookies)
var ipbMemberId: String? = null
var ipbPassHash: String? = null
var igneous: String? = null
var i = 0
val n = cookies.size
while (i < n) {
val cookie = cookies[i]
when (cookie.name) {
EhCookieStore.KEY_IPD_MEMBER_ID -> ipbMemberId = cookie.value
EhCookieStore.KEY_IPD_PASS_HASH -> ipbPassHash = cookie.value
EhCookieStore.KEY_IGNEOUS -> igneous = cookie.value
}
i++
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = (EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous)
setDialogMessage(
Html.fromHtml(
context.getString(
R.string.settings_eh_identity_cookies_signed,
message
), Html.FROM_HTML_MODE_LEGACY
)
)
message = message!!.replace("<br>", "\n")
} else {
setDialogMessage(context.getString(R.string.settings_eh_identity_cookies_tourist))
}
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
if (message != null) {
builder.setNeutralButton(R.string.settings_eh_identity_cookies_copy) { dialog: DialogInterface?, which: Int ->
mActivity.addTextToClipboard(message, true)
this@AccountPreference.onClick(dialog, which)
}
}
builder.setPositiveButton(R.string.settings_eh_sign_out) { _: DialogInterface?, _: Int ->
EhUtils.signOut(context)
mActivity.showTip(R.string.settings_eh_sign_out_tip, BaseScene.LENGTH_SHORT)
}
}
}
@@ -1,117 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* 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 com.hippo.ehviewer.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.util.AttributeSet;
import androidx.appcompat.app.AlertDialog;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.preference.MessagePreference;
import com.hippo.util.ClipboardUtil;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class IdentityCookiePreference extends MessagePreference {
@SuppressLint("StaticFieldLeak")
private final SettingsActivity mActivity;
private String message;
public IdentityCookiePreference(Context context) {
super(context);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivity = (SettingsActivity) context;
init();
}
private void init() {
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
List<Cookie> eCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_E));
List<Cookie> exCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_EX));
List<Cookie> cookies = new LinkedList<>(eCookies);
cookies.addAll(exCookies);
String ipbMemberId = null;
String ipbPassHash = null;
String igneous = null;
for (int i = 0, n = cookies.size(); i < n; i++) {
Cookie cookie = cookies.get(i);
switch (cookie.name()) {
case EhCookieStore.KEY_IPD_MEMBER_ID:
ipbMemberId = cookie.value();
break;
case EhCookieStore.KEY_IPD_PASS_HASH:
ipbPassHash = cookie.value();
break;
case EhCookieStore.KEY_IGNEOUS:
igneous = cookie.value();
break;
}
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous;
setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message), Html.FROM_HTML_MODE_LEGACY));
message = message.replace("<br>", "\n");
} else {
setDialogMessage(getContext().getString(R.string.settings_eh_identity_cookies_tourist));
}
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
if (message != null) {
builder.setPositiveButton(R.string.settings_eh_identity_cookies_copy, (dialog, which) -> {
ClipboardUtil.addTextToClipboard(message);
mActivity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT);
IdentityCookiePreference.this.onClick(dialog, which);
});
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setPositiveButton(android.R.string.ok, null);
}
}
}
@@ -1,73 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import androidx.appcompat.app.AlertDialog;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.preference.MessagePreference;
public class SignOutPreference extends MessagePreference {
@SuppressLint("StaticFieldLeak")
private final SettingsActivity mActivity;
public SignOutPreference(Context context) {
super(context);
init();
mActivity = (SettingsActivity) context;
}
public SignOutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
mActivity = (SettingsActivity) context;
}
public SignOutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
mActivity = (SettingsActivity) context;
}
private void init() {
setDialogMessage(getContext().getString(R.string.settings_eh_sign_out_warning));
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
builder.setPositiveButton(R.string.settings_eh_sign_out_yes, this);
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
EhUtils.signOut(getContext());
mActivity.showTip(R.string.settings_eh_sign_out_tip, BaseScene.LENGTH_SHORT);
}
}
}
@@ -18,14 +18,12 @@ package com.hippo.ehviewer.preference
import android.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.hippo.Native
import androidx.preference.Preference
import com.hippo.ehviewer.R
import com.hippo.preference.MessagePreference
class VersionPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MessagePreference(context, attrs) {
) : Preference(context, attrs) {
init {
setTitle(R.string.settings_about_version)
@Suppress("DEPRECATION") val version: String = try {
@@ -37,31 +35,4 @@ class VersionPreference @JvmOverloads constructor(
}
summary = version
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setTitle(R.string.show_library_version)
val info = NativeLibInfo().addLib(Native.getlibarchiveVersion())
.addLib("lzma(xz-utils)", Native.getliblzmaVersion())
.addLib("zlib", Native.getzlibVersion())
.addLib(Native.getnettleVersion())
builder.setMessage(info.getMessage())
}
inner class NativeLibInfo {
private var message: String = ""
fun addLib(libname: String, libversion: String): NativeLibInfo {
message = "$message$libname $libversion\n"
return this
}
fun getMessage(): String {
return message;
}
fun addLib(msg: String): NativeLibInfo {
message = "$message$msg\n"
return this
}
}
}
@@ -1678,9 +1678,7 @@ public final class SpiderQueen implements Runnable {
if (is != null) {
try {
image = Image.decode(((FileInputStream) is).getFD(), true);
} catch (IOException e) {
e.printStackTrace();
image = Image.decode(((FileInputStream) is), false);
} finally {
try {
is.close();
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -42,6 +43,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class CommonOperations {
@@ -110,8 +112,14 @@ public final class CommonOperations {
startDownload(activity, Collections.singletonList(galleryInfo), forceDefault);
}
// TODO Add context if activity and context are different style
public static void startDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Objects.requireNonNull(EhApplication.getInstance().getTopActivity()).checkAndRequestNotificationPermission();
}
doStartDownload(activity, galleryInfos, forceDefault);
}
private static void doStartDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
final DownloadManager dm = EhApplication.getDownloadManager(activity);
LongList toStart = new LongList();
@@ -1,138 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatActivity;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import rikka.core.res.ResourcesKt;
import rikka.insets.WindowInsetsHelper;
import rikka.layoutinflater.view.LayoutInflaterFactory;
public abstract class EhActivity extends AppCompatActivity {
private static final String THEME_DEFAULT = "DEFAULT";
private static final String THEME_BLACK = "BLACK";
public static boolean isBlackNightTheme() {
return Settings.getBoolean("black_dark_theme", false);
}
public static String getTheme(Context context) {
if (isBlackNightTheme() && isNightMode(context.getResources().getConfiguration()))
return THEME_BLACK;
return THEME_DEFAULT;
}
public static boolean isNightMode(Configuration configuration) {
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
}
@StyleRes
public int getThemeStyleRes(Context context) {
if (THEME_BLACK.equals(getTheme(context))) {
return R.style.ThemeOverlay_Black;
}
return R.style.ThemeOverlay;
}
@Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
if (getParent() == null) {
theme.applyStyle(resid, true);
} else {
try {
theme.setTo(getParent().getTheme());
} catch (Exception ignored) {
}
theme.applyStyle(resid, false);
}
theme.applyStyle(getThemeStyleRes(this), true);
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first);
}
@Override
protected void onNightModeChanged(int mode) {
getTheme().applyStyle(getThemeStyleRes(this), true);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
getLayoutInflater().setFactory2((new LayoutInflaterFactory(getDelegate())).addOnViewCreatedListener(WindowInsetsHelper.getLISTENER()));
super.onCreate(savedInstanceState);
Window window = getWindow();
window.setStatusBarColor(Color.TRANSPARENT);
window.getDecorView().post(() -> {
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.setNavigationBarDividerColor(getColor(R.color.navigation_bar_divider));
}
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(false);
}
} else {
window.setNavigationBarColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(true);
}
}
});
((EhApplication) getApplication()).registerActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
((EhApplication) getApplication()).unregisterActivity(this);
}
@Override
protected void onResume() {
if (Settings.getSecurity() && SecurityActivity.Companion.isAuthenticationSupported(this) && EhApplication.locked) {
startActivity(new Intent(this, SecurityActivity.class));
}
super.onResume();
if (Settings.getEnabledSecurity()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
}
@@ -0,0 +1,154 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.locked_last_leave_time
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import rikka.core.res.resolveColor
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class EhActivity : AppCompatActivity() {
@StyleRes
fun getThemeStyleRes(context: Context): Int {
return if (THEME_BLACK == getTheme(context)) {
R.style.ThemeOverlay_Black
} else R.style.ThemeOverlay
}
override fun onApplyThemeResource(theme: Theme, resid: Int, first: Boolean) {
if (parent == null) {
theme.applyStyle(resid, true)
} else {
try {
theme.setTo(parent.theme)
} catch (ignored: Exception) {
}
theme.applyStyle(resid, false)
}
theme.applyStyle(getThemeStyleRes(this), true)
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first)
}
override fun onNightModeChanged(mode: Int) {
theme.applyStyle(getThemeStyleRes(this), true)
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 =
LayoutInflaterFactory(delegate).addOnViewCreatedListener(
WindowInsetsHelper.LISTENER
)
super.onCreate(savedInstanceState)
window.statusBarColor = Color.TRANSPARENT
window.decorView.post {
val rootWindowInsets = window.decorView.rootWindowInsets
if (rootWindowInsets != null && rootWindowInsets.systemWindowInsetBottom >= Resources.getSystem().displayMetrics.density * 40) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
}
window.navigationBarColor =
theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
} else {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = true
}
}
}
(application as EhApplication).registerActivity(this)
}
override fun onDestroy() {
super.onDestroy()
(application as EhApplication).unregisterActivity(this)
}
override fun onResume() {
val locked_resume_time = System.currentTimeMillis() / 1000
val locked_delay_time = locked_resume_time - locked_last_leave_time
if (locked_delay_time < Settings.getSecurityDelay() * 60) {
EhApplication.locked = false
} else if (Settings.getSecurity() && isAuthenticationSupported() && EhApplication.locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
super.onResume()
if (Settings.getEnabledSecurity()) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {
Settings.putNotificationRequired()
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
fun checkAndRequestNotificationPermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
return
}
if (Settings.getNotificationRequired())
return
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
companion object {
private const val THEME_DEFAULT = "DEFAULT"
private const val THEME_BLACK = "BLACK"
private val isBlackNightTheme: Boolean
get() = Settings.getBoolean("black_dark_theme", false)
fun getTheme(context: Context): String {
return if (isBlackNightTheme && isNightMode(context.resources.configuration)) THEME_BLACK else THEME_DEFAULT
}
private fun isNightMode(configuration: Configuration): Boolean {
return configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0
}
}
}
@@ -67,8 +67,8 @@ import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.BuildConfig;
@@ -290,16 +290,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
outState.putInt(KEY_PAGE, mPage);
outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex);
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result && mSavingPage != -1) {
saveImage(mSavingPage);
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
}
mSavingPage = -1;
});
}
@Override
protected void attachBaseContext(@NonNull Context newBase) {
@@ -404,21 +395,25 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
if (mGalleryView == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL:
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else {
mGalleryView.pageRight();
}
} else {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageRight();
} else {
mGalleryView.pageLeft();
}
if (event.getAction() == MotionEvent.ACTION_SCROLL) {
float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300;
if (scroll < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
} else {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
}
}
return false;
});
@@ -516,7 +511,16 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
mSeekBar = null;
SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable);
}
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result && mSavingPage != -1) {
saveImage(mSavingPage);
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
}
mSavingPage = -1;
});
@Override
protected void onPause() {
@@ -853,7 +857,8 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
intent.setAction(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.putExtra(Intent.EXTRA_TEXT, EhUrl.getGalleryDetailUrl(mGalleryInfo.gid, mGalleryInfo.token));
if (mGalleryInfo != null)
intent.putExtra(Intent.EXTRA_TEXT, EhUrl.getGalleryDetailUrl(mGalleryInfo.gid, mGalleryInfo.token));
intent.setDataAndType(uri, mimeType);
try {
@@ -985,7 +990,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
private void showPageDialog(final int page) {
Resources resources = GalleryActivity.this.getResources();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this);
BaseDialogBuilder builder = new BaseDialogBuilder(GalleryActivity.this);
builder.setTitle(resources.getString(R.string.page_menu_title, page + 1));
final CharSequence[] items;
@@ -1281,7 +1286,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
private void onTapMenuArea() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this);
BaseDialogBuilder builder = new BaseDialogBuilder(GalleryActivity.this);
GalleryMenuHelper helper = new GalleryMenuHelper(builder.getContext());
builder.setTitle(R.string.gallery_menu_title)
.setView(helper.getView())
@@ -1370,4 +1375,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
}
@@ -0,0 +1,194 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.util.addTextToClipboard
class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment() {
private var mKeys: ArrayList<String> = arrayListOf()
private var mValues: ArrayList<String> = arrayListOf()
private var mRecyclerView: RecyclerView? = null
private val mDetail: GalleryDetail = detail
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
parseDetail()
val view = inflater.inflate(R.layout.scene_gallery_info, container, false)
dialog?.window?.run {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes.blurBehindRadius = 32
}
}
val recyclerView = view.findViewById(R.id.recycler_view) as RecyclerView
val adapter = InfoAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.clipToPadding = false
recyclerView.setHasFixedSize(true)
mRecyclerView = recyclerView
return view
}
private fun parseDetail() {
mKeys.add(getString(R.string.header_key))
mValues.add(getString(R.string.header_value))
mKeys.add(getString(R.string.key_gid))
mValues.add(mDetail.gid.toString())
mKeys.add(getString(R.string.key_token))
mValues.add(mDetail.token)
mKeys.add(getString(R.string.key_url))
mValues.add(EhUrl.getGalleryDetailUrl(mDetail.gid, mDetail.token))
mKeys.add(getString(R.string.key_title))
mValues.add(mDetail.title)
mKeys.add(getString(R.string.key_title_jpn))
mValues.add(mDetail.titleJpn)
mKeys.add(getString(R.string.key_thumb))
mValues.add(mDetail.thumb)
mKeys.add(getString(R.string.key_category))
mValues.add(EhUtils.getCategory(mDetail.category))
mKeys.add(getString(R.string.key_uploader))
mValues.add(mDetail.uploader)
mKeys.add(getString(R.string.key_posted))
mValues.add(mDetail.posted)
mKeys.add(getString(R.string.key_parent))
mValues.add(mDetail.parent)
mKeys.add(getString(R.string.key_visible))
mValues.add(mDetail.visible)
mKeys.add(getString(R.string.key_language))
mValues.add(mDetail.language)
mKeys.add(getString(R.string.key_pages))
mValues.add(mDetail.pages.toString())
mKeys.add(getString(R.string.key_size))
mValues.add(mDetail.size)
mKeys.add(getString(R.string.key_favorite_count))
mValues.add(mDetail.favoriteCount.toString())
mKeys.add(getString(R.string.key_favorited))
mValues.add(java.lang.Boolean.toString(mDetail.isFavorited))
mKeys.add(getString(R.string.key_rating_count))
mValues.add(mDetail.ratingCount.toString())
mKeys.add(getString(R.string.key_rating))
mValues.add(mDetail.rating.toString())
mKeys.add(getString(R.string.key_torrents))
mValues.add(mDetail.torrentCount.toString())
mKeys.add(getString(R.string.key_torrent_url))
mValues.add(mDetail.torrentUrl)
mKeys.add(getString(R.string.favorite_name))
mValues.add(mDetail.favoriteName)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(view.findViewById(R.id.title) as TextView).setText(R.string.gallery_info)
}
override fun onDestroyView() {
super.onDestroyView()
mRecyclerView?.stopScroll()
}
fun onItemClick(position: Int): Boolean {
val context = context
return if (null != context && 0 != position) {
if (position == INDEX_PARENT) {
UrlOpener.openUrl(context, mValues[position], true)
} else {
activity?.addTextToClipboard(
mValues[position],
false,
)
if (position == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(mValues[position].hashCode())
}
}
true
} else {
false
}
}
private class InfoHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val key: TextView
val value: TextView
init {
key = itemView.findViewById(R.id.key)
value = itemView.findViewById(R.id.value)
}
}
private inner class InfoAdapter : RecyclerView.Adapter<InfoHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun getItemViewType(position: Int): Int {
return if (position == 0) {
TYPE_HEADER
} else {
TYPE_DATA
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder {
return InfoHolder(
mInflater.inflate(
if (viewType == TYPE_HEADER) R.layout.item_gallery_info_header else R.layout.item_gallery_info_data,
parent,
false
)
)
}
override fun onBindViewHolder(holder: InfoHolder, position: Int) {
holder.key.text = mKeys[position]
holder.value.text = mValues[position]
holder.itemView.isEnabled = position != 0
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return mKeys.size.coerceAtMost(mValues.size)
}
}
companion object {
const val TAG = "GalleryInfoBottomSheet"
private const val INDEX_URL = 3
private const val INDEX_PARENT = 10
private const val TYPE_HEADER = 0
private const val TYPE_DATA = 1
}
}
@@ -42,9 +42,9 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.drawerlayout.widget.DrawerLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.R;
@@ -61,7 +61,6 @@ import com.hippo.ehviewer.ui.scene.DownloadsScene;
import com.hippo.ehviewer.ui.scene.FavoritesScene;
import com.hippo.ehviewer.ui.scene.GalleryCommentsScene;
import com.hippo.ehviewer.ui.scene.GalleryDetailScene;
import com.hippo.ehviewer.ui.scene.GalleryInfoScene;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.ehviewer.ui.scene.GalleryPreviewsScene;
import com.hippo.ehviewer.ui.scene.HistoryScene;
@@ -77,7 +76,7 @@ import com.hippo.scene.SceneFragment;
import com.hippo.scene.StageActivity;
import com.hippo.unifile.UniFile;
import com.hippo.util.BitmapUtils;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.widget.DrawerView;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.SimpleHandler;
@@ -101,7 +100,6 @@ public final class MainActivity extends StageActivity
registerLaunchMode(SelectSiteScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(GalleryListScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TOP);
registerLaunchMode(GalleryDetailScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryInfoScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryCommentsScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryPreviewsScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(DownloadsScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
@@ -232,7 +230,6 @@ public final class MainActivity extends StageActivity
builder.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
builder.setImagePath(temp.getPath());
builder.setUseSimilarityScan(true);
builder.setShowExpunged(true);
startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)));
return true;
}
@@ -259,7 +256,7 @@ public final class MainActivity extends StageActivity
String url = intent.getData().toString();
new EditTextDialogBuilder(this, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy, (dialog, which) -> ClipboardUtil.addTextToClipboard(url))
.setPositiveButton(android.R.string.copy, (dialog, which) -> ClipboardUtilKt.addTextToClipboard(this, url, false))
.show();
}
}
@@ -288,7 +285,6 @@ public final class MainActivity extends StageActivity
mStageLayout = (EhStageLayout) ViewUtils.$$(this, R.id.fragment_container);
mDrawerLayout = (DrawerLayout) ViewUtils.$$(this, R.id.draw_view);
mDrawerLayout.setStatusBarBackgroundColor(0);
mNavView = (NavigationView) ViewUtils.$$(this, R.id.nav_view);
mRightDrawer = (DrawerView) ViewUtils.$$(this, R.id.right_drawer);
@@ -338,7 +334,7 @@ public final class MainActivity extends StageActivity
break;
}
if (hasUnverified) {
new MaterialAlertDialogBuilder(this)
new BaseDialogBuilder(this)
.setTitle(R.string.app_link_not_verified_title)
.setMessage(R.string.app_link_not_verified_message)
.setPositiveButton(R.string.open_settings, (dialogInterface, i) -> {
@@ -364,7 +360,7 @@ public final class MainActivity extends StageActivity
if (null == uniFile || uniFile.ensureDir()) {
return;
}
new MaterialAlertDialogBuilder(this)
new BaseDialogBuilder(this)
.setTitle(R.string.waring)
.setMessage(R.string.invalid_download_location)
.setPositiveButton(R.string.get_it, null)
@@ -457,7 +453,7 @@ public final class MainActivity extends StageActivity
}
private void checkClipboardUrlInternal() {
String text = ClipboardUtil.getUrlFromClipboard();
String text = ClipboardUtilKt.getUrlFromClipboard(ClipboardUtilKt.getClipboardManager(this), this);
int hashCode = text != null ? text.hashCode() : 0;
if (text != null && hashCode != 0 && Settings.getClipboardTextHashCode() != hashCode) {
@@ -481,8 +477,7 @@ public final class MainActivity extends StageActivity
@SuppressLint("RtlHardcoded")
public void createDrawerView(SceneFragment scene) {
if (scene instanceof BaseScene && mRightDrawer != null && mDrawerLayout != null) {
BaseScene baseScene = (BaseScene) scene;
if (scene instanceof BaseScene baseScene && mRightDrawer != null && mDrawerLayout != null) {
mRightDrawer.removeAllViews();
View drawerView = baseScene.createDrawerView(
baseScene.getLayoutInflater(), mRightDrawer, null);
@@ -499,8 +494,7 @@ public final class MainActivity extends StageActivity
public void onSceneViewDestroyed(SceneFragment scene) {
super.onSceneViewDestroyed(scene);
if (scene instanceof BaseScene) {
BaseScene baseScene = (BaseScene) scene;
if (scene instanceof BaseScene baseScene) {
baseScene.destroyDrawerView();
}
}
@@ -19,7 +19,6 @@
package com.hippo.ehviewer.ui
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
@@ -31,10 +30,16 @@ import androidx.fragment.app.FragmentActivity
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
fun Context.isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(this)
.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
class SecurityActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAuthenticationSupported(this))
override fun onResume() {
super.onResume()
if (isAuthenticationSupported())
startAuthentication(getString(R.string.settings_privacy_require_unlock))
else
onSuccess()
@@ -71,14 +76,6 @@ class SecurityActivity : AppCompatActivity() {
}
}
companion object {
fun isAuthenticationSupported(context: Context): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(context)
.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
}
private fun onSuccess() {
EhApplication.locked = false
finish()
@@ -1,68 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.fragment.SettingsFragment;
import com.hippo.ehviewer.ui.scene.BaseScene;
public final class SettingsActivity extends EhActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preference);
setSupportActionBar(findViewById(R.id.toolbar));
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setDisplayHomeAsUpEnabled(true);
}
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
.replace(R.id.fragment, new SettingsFragment())
.commitAllowingStateLoss();
}
}
public void showTip(@StringRes int id, int length) {
showTip(getString(id), length);
}
public void showTip(CharSequence message, int length) {
Snackbar.make(findViewById(R.id.snackbar), message,
length == BaseScene.LENGTH_LONG ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui
import android.content.res.Configuration
import android.os.Bundle
import android.view.MenuItem
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.fragment.SettingsFragment
import com.hippo.ehviewer.ui.scene.BaseScene
class SettingsActivity : EhActivity() {
private var mFab: FloatingActionButton? = null
private var mContentText: TextView? = null
private var mAppbarLayout: AppBarLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preference)
setSupportActionBar(findViewById(R.id.toolbar))
mContentText = findViewById(R.id.tip)
mFab = findViewById(R.id.fab)
mAppbarLayout = findViewById(R.id.appbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) <= 0
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
.replace(R.id.fragment, SettingsFragment())
.commitAllowingStateLoss()
}
}
fun showTip(@StringRes id: Int, length: Int) {
showTip(getString(id), length)
}
fun showTip(message: CharSequence?, length: Int) {
Snackbar.make(
findViewById(R.id.snackbar), message!!,
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
).show()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressedDispatcher.onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
fun getMFab(): FloatingActionButton {
return mFab!!
}
fun getMContentText(): TextView {
return mContentText!!
}
fun resetAppbarLiftStatus() {
mAppbarLayout?.setExpanded(true)
}
}
@@ -22,6 +22,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -31,13 +32,18 @@ import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.os.LocaleListCompat;
import androidx.preference.Preference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.BuildConfig;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.data.FavListUrlBuilder;
import com.hippo.ehviewer.client.parser.FavoritesParser;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
@@ -58,12 +64,11 @@ import java.util.zip.ZipOutputStream;
public class AdvancedFragment extends BasePreferenceFragment {
private static final String KEY_DUMP_LOGCAT = "dump_logcat";
private static final String KEY_CLEAR_MEMORY_CACHE = "clear_memory_cache";
private static final String KEY_APP_LANGUAGE = "app_language";
private static final String KEY_IMPORT_DATA = "import_data";
private static final String KEY_EXPORT_DATA = "export_data";
private static final String KEY_OPEN_BY_DEFAULT = "open_by_default";
private static final String KEY_BACKUP_FAVORITE = "backup_favorite";
ActivityResultLauncher<String> exportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument(),
uri -> {
@@ -76,7 +81,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
e.printStackTrace();
}
try {
AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireActivity())
AlertDialog alertDialog = new BaseDialogBuilder(requireActivity())
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show();
@@ -193,7 +198,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
e.printStackTrace();
}
try {
AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireActivity())
AlertDialog alertDialog = new BaseDialogBuilder(requireActivity())
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show();
@@ -219,16 +224,20 @@ public class AdvancedFragment extends BasePreferenceFragment {
}
}
});
private int favTotal;
private int favIndex;
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.advanced_settings);
Preference dumpLogcat = findPreference(KEY_DUMP_LOGCAT);
Preference clearMemoryCache = findPreference(KEY_CLEAR_MEMORY_CACHE);
Preference appLanguage = findPreference(KEY_APP_LANGUAGE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
appLanguage.setVisible(false);
Preference importData = findPreference(KEY_IMPORT_DATA);
Preference exportData = findPreference(KEY_EXPORT_DATA);
Preference backupFavorite = findPreference(KEY_BACKUP_FAVORITE);
Preference openByDefault = findPreference(KEY_OPEN_BY_DEFAULT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
@@ -238,9 +247,9 @@ public class AdvancedFragment extends BasePreferenceFragment {
}
dumpLogcat.setOnPreferenceClickListener(this);
clearMemoryCache.setOnPreferenceClickListener(this);
importData.setOnPreferenceClickListener(this);
exportData.setOnPreferenceClickListener(this);
backupFavorite.setOnPreferenceClickListener(this);
appLanguage.setOnPreferenceChangeListener(this);
}
@@ -256,10 +265,6 @@ public class AdvancedFragment extends BasePreferenceFragment {
showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT);
}
return true;
} else if (KEY_CLEAR_MEMORY_CACHE.equals(key)) {
((EhApplication) requireContext().getApplicationContext()).clearMemoryCache();
Runtime.getRuntime().gc();
showTip(R.string.settings_advanced_clear_memory_cache_done, BaseScene.LENGTH_SHORT);
} else if (KEY_IMPORT_DATA.equals(key)) {
try {
importDataLauncher.launch(new String[]{"*/*"});
@@ -276,6 +281,14 @@ public class AdvancedFragment extends BasePreferenceFragment {
showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT);
}
return true;
} else if (KEY_BACKUP_FAVORITE.equals(key)) {
try {
backupFavorite();
} catch (Exception e) {
ExceptionUtils.throwIfFatal(e);
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
return true;
} else if (KEY_OPEN_BY_DEFAULT.equals(key)) {
try {
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
@@ -291,10 +304,73 @@ public class AdvancedFragment extends BasePreferenceFragment {
return false;
}
private void backupFavorite() {
EhClient mClient = EhApplication.getEhClient(requireContext());
FavListUrlBuilder favListUrlBuilder = new FavListUrlBuilder();
favTotal = 0;
favIndex = 1;
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
request.setCallback(new EhClient.Callback() {
@Override
public void onSuccess(Object result) {
try {
FavoritesParser.Result favResult = (FavoritesParser.Result) result;
if (favResult.galleryInfoList.isEmpty()) {
showTip(R.string.settings_advanced_backup_favorite_nothing, BaseScene.LENGTH_SHORT);
} else {
if (favTotal == 0 && favResult.countArray != null) {
int totalFav = 0;
for (int i = 0; i < 10; i++) {
totalFav = totalFav + favResult.countArray[i];
}
favTotal = (int) Math.ceil((double) totalFav / favResult.galleryInfoList.size());
}
String status = "(" + favIndex + "/" + favTotal + ")";
showTip(GetText.getString(R.string.settings_advanced_backup_favorite_start, status), BaseScene.LENGTH_SHORT);
Log.d("LocalFavorites", "now backup page " + status);
EhDB.putLocalFavorites(favResult.galleryInfoList);
if (favResult.nextPage != null) {
try {
Thread.sleep(Settings.getDownloadDelay());
} catch (InterruptedException e) {
e.printStackTrace();
}
favIndex++;
favListUrlBuilder.setNext(favResult.nextPage);
request.setArgs(favListUrlBuilder.build(), Settings.getShowJpnTitle());
mClient.execute(request);
} else {
showTip(R.string.settings_advanced_backup_favorite_success, BaseScene.LENGTH_SHORT);
}
}
} catch (Exception e) {
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
}
@Override
public void onFailure(Exception e) {
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
@Override
public void onCancel() {
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
});
request.setArgs(favListUrlBuilder.build(), Settings.getShowJpnTitle());
mClient.execute(request);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey();
if (KEY_APP_LANGUAGE.equals(key)) {
if (KEY_APP_LANGUAGE.equals(key) && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if ("system".equals(newValue)) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
} else {
@@ -1,50 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import com.hippo.ehviewer.ui.SettingsActivity;
public class BaseFragment extends Fragment {
@Override
public void onStart() {
super.onStart();
setTitle(getFragmentTitle());
}
@StringRes
public int getFragmentTitle() {
return -1;
}
private void setTitle(@StringRes int string) {
requireActivity().setTitle(string);
}
public void showTip(@StringRes int id, int length) {
((SettingsActivity) requireActivity()).showTip(getString(id), length);
}
public void showTip(CharSequence message, int length) {
((SettingsActivity) requireActivity()).showTip(message, length);
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.ehviewer.ui.SettingsActivity
abstract class BaseFragment : Fragment() {
override fun onStart() {
super.onStart()
setTitle(getFragmentTitle())
(requireActivity() as SettingsActivity).resetAppbarLiftStatus()
}
override fun onDestroyView() {
hideFabAndClearTip()
super.onDestroyView()
}
abstract fun getFragmentTitle(): Int
private fun setTitle(@StringRes string: Int) {
requireActivity().setTitle(string)
}
fun showTip(@StringRes id: Int, length: Int) {
(requireActivity() as SettingsActivity).showTip(getString(id), length)
}
fun showTip(message: CharSequence?, length: Int) {
(requireActivity() as SettingsActivity).showTip(message, length)
}
fun getTipView(@StringRes res: Int? = null): TextView {
return (requireActivity() as SettingsActivity).getMContentText().apply {
visibility = View.VISIBLE
res?.let { text = getString(it) }
}
}
fun getFabViewAndShow(): FloatingActionButton {
return (requireActivity() as SettingsActivity).getMFab().apply { visibility = View.VISIBLE }
}
private fun hideFabAndClearTip() {
getFabViewAndShow().apply {
visibility = View.GONE
setOnClickListener(null)
}
getTipView().apply {
visibility = View.GONE
}
}
}
@@ -70,7 +70,8 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
.replace(R.id.fragment, classObj)
.addToBackStack(null)
.commitAllowingStateLoss();
} catch (ClassNotFoundException | IllegalAccessException | java.lang.InstantiationException ignored) {
} catch (ClassNotFoundException | IllegalAccessException |
java.lang.InstantiationException ignored) {
}
return true;
}
@@ -25,7 +25,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
@@ -99,7 +99,7 @@ public class DownloadFragment extends BasePreferenceFragment {
if (KEY_DOWNLOAD_LOCATION.equals(key)) {
UniFile file = Settings.getDownloadLocation();
if (file != null && !UniFile.isFileUri(Settings.getDownloadLocation().getUri())) {
new MaterialAlertDialogBuilder(requireContext())
new BaseDialogBuilder(requireContext())
.setTitle(R.string.settings_download_download_location)
.setMessage(file.getUri().toString())
.setPositiveButton(R.string.pick_new_download_location, (dialogInterface, i) -> openDirPickerL())
@@ -1,94 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.fragment;
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.Preference;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhTagDatabase;
public class EhFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.eh_settings);
Preference theme = findPreference(Settings.KEY_THEME);
Preference blackDarkTheme = findPreference(Settings.KEY_BLACK_DARK_THEME);
Preference gallerySite = findPreference(Settings.KEY_GALLERY_SITE);
Preference listMode = findPreference(Settings.KEY_LIST_MODE);
Preference detailSize = findPreference(Settings.KEY_DETAIL_SIZE);
Preference thumbSize = findPreference(Settings.KEY_THUMB_SIZE);
Preference showTagTranslations = findPreference(Settings.KEY_SHOW_TAG_TRANSLATIONS);
Preference tagTranslationsSource = findPreference("tag_translations_source");
theme.setOnPreferenceChangeListener(this);
gallerySite.setOnPreferenceChangeListener(this);
listMode.setOnPreferenceChangeListener(this);
detailSize.setOnPreferenceChangeListener(this);
thumbSize.setOnPreferenceChangeListener(this);
showTagTranslations.setOnPreferenceChangeListener(this);
blackDarkTheme.setOnPreferenceChangeListener(this);
if (!EhTagDatabase.isPossible(requireActivity())) {
getPreferenceScreen().removePreference(showTagTranslations);
getPreferenceScreen().removePreference(tagTranslationsSource);
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey();
if (Settings.KEY_THEME.equals(key)) {
AppCompatDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
requireActivity().recreate();
return true;
} else if (Settings.KEY_GALLERY_SITE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
return true;
} else if (Settings.KEY_LIST_MODE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
return true;
} else if (Settings.KEY_DETAIL_SIZE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
} else if (Settings.KEY_THUMB_SIZE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
} else if (Settings.KEY_SHOW_TAG_TRANSLATIONS.equals(key)) {
if (Boolean.TRUE.equals(newValue)) {
EhTagDatabase.update(requireActivity());
}
} else if (Settings.KEY_BLACK_DARK_THEME.equals(key)) {
if ((requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) > 0) {
requireActivity().recreate();
}
return true;
}
return true;
}
@Override
public int getFragmentTitle() {
return R.string.settings_eh;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.fragment
import android.app.Activity
import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.Preference
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase
class EhFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.eh_settings)
val account = findPreference<Preference>(Settings.KEY_ACCOUNT)
val theme = findPreference<Preference>(Settings.KEY_THEME)
val blackDarkTheme = findPreference<Preference>(Settings.KEY_BLACK_DARK_THEME)
val gallerySite = findPreference<Preference>(Settings.KEY_GALLERY_SITE)
val listMode = findPreference<Preference>(Settings.KEY_LIST_MODE)
val listThumbSize = findPreference<Preference>(Settings.KEY_LIST_THUMB_SIZE)
val detailSize = findPreference<Preference>(Settings.KEY_DETAIL_SIZE)
val thumbSize = findPreference<Preference>(Settings.KEY_THUMB_SIZE)
val showTagTranslations = findPreference<Preference>(Settings.KEY_SHOW_TAG_TRANSLATIONS)
val tagTranslationsSource = findPreference<Preference>("tag_translations_source")
Settings.getDisplayName()?.let { account?.summary = it }
theme!!.onPreferenceChangeListener = this
gallerySite!!.onPreferenceChangeListener = this
listMode!!.onPreferenceChangeListener = this
listThumbSize!!.onPreferenceChangeListener = this
detailSize!!.onPreferenceChangeListener = this
thumbSize!!.onPreferenceChangeListener = this
showTagTranslations!!.onPreferenceChangeListener = this
blackDarkTheme!!.onPreferenceChangeListener = this
if (!EhTagDatabase.isPossible(requireActivity())) {
preferenceScreen.removePreference(showTagTranslations)
preferenceScreen.removePreference(tagTranslationsSource!!)
}
}
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
val key = preference.key
if (Settings.KEY_THEME == key) {
AppCompatDelegate.setDefaultNightMode((newValue as String).toInt())
requireActivity().recreate()
return true
} else if (Settings.KEY_GALLERY_SITE == key) {
requireActivity().setResult(Activity.RESULT_OK)
return true
} else if (Settings.KEY_LIST_MODE == key) {
requireActivity().setResult(Activity.RESULT_OK)
return true
} else if (Settings.KEY_LIST_THUMB_SIZE == key) {
Settings.LIST_THUMB_SIZE_INITED = false
requireActivity().setResult(Activity.RESULT_OK)
return true
} else if (Settings.KEY_DETAIL_SIZE == key) {
requireActivity().setResult(Activity.RESULT_OK)
} else if (Settings.KEY_THUMB_SIZE == key) {
requireActivity().setResult(Activity.RESULT_OK)
} else if (Settings.KEY_SHOW_TAG_TRANSLATIONS == key) {
if (java.lang.Boolean.TRUE == newValue) {
EhTagDatabase.update(requireActivity())
}
} else if (Settings.KEY_BLACK_DARK_THEME == key) {
if (requireActivity().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0) {
requireActivity().recreate()
}
return true
}
return true
}
override fun getFragmentTitle(): Int {
return R.string.settings_eh
}
}
@@ -20,7 +20,12 @@ package com.hippo.ehviewer.ui.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
@@ -31,10 +36,8 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.dao.Filter
@@ -66,26 +69,23 @@ class FilterFragment : BaseFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.activity_filter, container, false)
val recyclerView: RecyclerView =
ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView
val tip = ViewUtils.`$$`(view, R.id.tip) as TextView
): View {
val recyclerView =
inflater.inflate(R.layout.rv_layout, container, false) as RecyclerView
val tip = getTipView(R.string.filter)
mViewTransition = ViewTransition(recyclerView, tip)
val fab = view.findViewById<FloatingActionButton>(R.id.fab)
val fab = getFabViewAndShow()
val drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable?.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mAdapter.setHasStableIds(true)
recyclerView.adapter = mAdapter
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.setHasFixedSize(true)
val defaultItemAnimator = recyclerView.itemAnimator as DefaultItemAnimator?
defaultItemAnimator?.supportsChangeAnimations = false
fab.setOnClickListener { showAddFilterDialog() }
return view
return recyclerView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -106,13 +106,13 @@ class FilterFragment : BaseFragment() {
}
override fun onDestroyView() {
super.onDestroyView()
mViewTransition = null
requireActivity().removeMenuProvider(mMenuProvider)
super.onDestroyView()
}
private fun showTipDialog() {
MaterialAlertDialogBuilder(requireActivity())
BaseDialogBuilder(requireActivity())
.setTitle(R.string.filter)
.setMessage(R.string.filter_tip)
.setPositiveButton(android.R.string.ok, null)
@@ -120,7 +120,7 @@ class FilterFragment : BaseFragment() {
}
private fun showAddFilterDialog() {
val dialog = MaterialAlertDialogBuilder(requireActivity())
val dialog = BaseDialogBuilder(requireActivity())
.setTitle(R.string.add_filter)
.setView(R.layout.dialog_add_filter)
.setPositiveButton(R.string.add, null)
@@ -131,7 +131,7 @@ class FilterFragment : BaseFragment() {
private fun showDeleteFilterDialog(filter: Filter) {
val message = getString(R.string.delete_filter, filter.text)
MaterialAlertDialogBuilder(requireActivity())
BaseDialogBuilder(requireActivity())
.setMessage(message)
.setPositiveButton(R.string.delete) { _: DialogInterface?, which: Int ->
if (DialogInterface.BUTTON_POSITIVE != which) {
@@ -1,282 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Hosts;
import com.hippo.ehviewer.R;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.ViewUtils;
import java.util.List;
import java.util.Locale;
public class HostsFragment extends BaseFragment
implements View.OnClickListener {
private static final String DIALOG_TAG_ADD_HOST = AddHostDialogFragment.class.getName();
private static final String DIALOG_TAG_EDIT_HOST = EditHostDialogFragment.class.getName();
private static final String KEY_HOST = "com.hippo.ehviewer.ui.fragment.HostsFragment.HOST";
private static final String KEY_IP = "com.hippo.ehviewer.ui.fragment.HostsFragment.IP";
private Hosts hosts;
private List<Pair<String, String>> data;
private ViewTransition mViewTransition;
private HostsAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hosts = EhApplication.getHosts(requireContext());
data = hosts.getAll();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.activity_hosts, container, false);
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(recyclerView, tip);
FloatingActionButton fab = view.findViewById(R.id.fab);
adapter = new HostsAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false));
recyclerView.setHasFixedSize(true);
fab.setOnClickListener(this);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
updateView(false);
}
public boolean onItemClick(int position) {
Pair<String, String> pair = data.get(position);
Bundle args = new Bundle();
args.putString(KEY_HOST, pair.first);
args.putString(KEY_IP, pair.second);
DialogFragment fragment = new EditHostDialogFragment();
fragment.setArguments(args);
fragment.show(getChildFragmentManager(), DIALOG_TAG_EDIT_HOST);
return true;
}
@Override
public void onClick(View v) {
new AddHostDialogFragment().show(getChildFragmentManager(), DIALOG_TAG_ADD_HOST);
}
private void updateView(boolean animation) {
if (null == mViewTransition) {
return;
}
data = hosts.getAll();
if (data.isEmpty()) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
adapter.notifyDataSetChanged();
}
@Override
public int getFragmentTitle() {
return R.string.hosts;
}
public abstract static class HostDialogFragment extends DialogFragment {
private HostsFragment hostsFragment;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
hostsFragment = (HostsFragment) getParentFragment();
View view = getLayoutInflater().inflate(R.layout.dialog_hosts, null, false);
TextView host = view.findViewById(R.id.host);
TextView ip = view.findViewById(R.id.ip);
Bundle arguments = getArguments();
if (savedInstanceState == null && arguments != null) {
host.setText(arguments.getString(KEY_HOST));
ip.setText(arguments.getString(KEY_IP));
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()).setView(view);
onCreateDialogBuilder(builder);
AlertDialog dialog = builder.create();
dialog.setOnShowListener(d -> onCreateDialog((AlertDialog) d));
return dialog;
}
protected abstract void onCreateDialogBuilder(AlertDialog.Builder builder);
protected abstract void onCreateDialog(AlertDialog dialog);
protected void put(AlertDialog dialog) {
TextView host = dialog.findViewById(R.id.host);
TextView ip = dialog.findViewById(R.id.ip);
if (host == null || ip == null) {
return;
}
String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
String ipString = ip.getText().toString().trim();
if (!Hosts.isValidHost(hostString)) {
TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout);
if (hostInputLayout == null) {
return;
}
hostInputLayout.setError(getString(R.string.invalid_host));
return;
}
if (!Hosts.isValidIp(ipString)) {
TextInputLayout ipInputLayout = dialog.findViewById(R.id.ip_input_layout);
if (ipInputLayout == null) {
return;
}
ipInputLayout.setError(getString(R.string.invalid_ip));
return;
}
hostsFragment.hosts.put(hostString, ipString);
hostsFragment.updateView(true);
dialog.dismiss();
}
protected void delete(AlertDialog dialog) {
TextView host = dialog.findViewById(R.id.host);
if (host == null) {
return;
}
String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
hostsFragment.hosts.delete(hostString);
hostsFragment.updateView(true);
dialog.dismiss();
}
}
public static class AddHostDialogFragment extends HostDialogFragment {
@Override
protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(R.string.add_host);
builder.setPositiveButton(R.string.add_host_add, null);
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onCreateDialog(AlertDialog dialog) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
}
}
public static class EditHostDialogFragment extends HostDialogFragment {
@Override
protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(R.string.edit_host);
builder.setPositiveButton(R.string.edit_host_confirm, null);
builder.setNegativeButton(R.string.edit_host_delete, null);
}
@Override
protected void onCreateDialog(AlertDialog dialog) {
TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout);
if (hostInputLayout == null) {
return;
}
hostInputLayout.setEnabled(false);
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> delete(dialog));
}
}
private static class HostsHolder extends RecyclerView.ViewHolder {
public final TextView host;
public final TextView ip;
public HostsHolder(View itemView) {
super(itemView);
host = itemView.findViewById(R.id.host);
ip = itemView.findViewById(R.id.ip);
}
}
private class HostsAdapter extends RecyclerView.Adapter<HostsHolder> {
private final LayoutInflater inflater = getLayoutInflater();
@NonNull
@Override
public HostsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false));
}
@Override
public void onBindViewHolder(@NonNull HostsHolder holder, int position) {
Pair<String, String> pair = data.get(position);
holder.host.setText(pair.first);
holder.ip.setText(pair.second);
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
}
@@ -0,0 +1,228 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer 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.
*
* EhViewer 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 EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Pair
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.Hosts
import com.hippo.ehviewer.R
import com.hippo.view.ViewTransition
class HostsFragment : BaseFragment(), View.OnClickListener {
private var hosts: Hosts? = null
private var data: List<Pair<String, String>>? = null
private var mViewTransition: ViewTransition? = null
private var adapter: HostsAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EhApplication.getHosts(requireContext()).apply {
hosts = this
data = all
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val recyclerView = inflater.inflate(R.layout.rv_layout, container, false) as RecyclerView
val tip = getTipView(R.string.hosts)
mViewTransition = ViewTransition(recyclerView, tip)
val fab = getFabViewAndShow()
adapter = HostsAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager =
LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false)
recyclerView.setHasFixedSize(true)
fab.setOnClickListener(this)
return recyclerView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateView(false)
}
fun onItemClick(position: Int): Boolean {
val pair = data!![position]
val args = Bundle()
args.putString(KEY_HOST, pair.first)
args.putString(KEY_IP, pair.second)
val fragment: DialogFragment = EditHostDialogFragment()
fragment.arguments = args
fragment.show(childFragmentManager, DIALOG_TAG_EDIT_HOST)
return true
}
override fun onClick(v: View) {
AddHostDialogFragment().show(childFragmentManager, DIALOG_TAG_ADD_HOST)
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
data = hosts!!.all
if ((data as MutableList<Pair<String, String>>?)?.isEmpty() == true) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
adapter!!.notifyDataSetChanged()
}
override fun getFragmentTitle(): Int {
return R.string.hosts
}
abstract class HostDialogFragment : DialogFragment() {
private var hostsFragment: HostsFragment? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
hostsFragment = parentFragment as HostsFragment?
val view = layoutInflater.inflate(R.layout.dialog_hosts, null, false)
val host = view.findViewById<TextView>(R.id.host)
val ip = view.findViewById<TextView>(R.id.ip)
val arguments = arguments
if (savedInstanceState == null && arguments != null) {
host.text = arguments.getString(KEY_HOST)
ip.text = arguments.getString(KEY_IP)
}
val builder = BaseDialogBuilder(requireContext())
builder.setView(view)
onCreateDialogBuilder(builder)
val dialog = builder.create()
dialog.setOnShowListener { d: DialogInterface -> onCreateDialog(d as AlertDialog) }
return dialog
}
protected abstract fun onCreateDialogBuilder(builder: AlertDialog.Builder)
protected abstract fun onCreateDialog(dialog: AlertDialog)
protected fun put(dialog: AlertDialog) {
val host = dialog.findViewById<TextView>(R.id.host)
val ip = dialog.findViewById<TextView>(R.id.ip)
if (host == null || ip == null) {
return
}
val hostString = host.text.toString().trim { it <= ' ' }.lowercase()
val ipString = ip.text.toString().trim { it <= ' ' }
if (!Hosts.isValidHost(hostString)) {
val hostInputLayout =
dialog.findViewById<TextInputLayout>(R.id.host_input_layout) ?: return
hostInputLayout.error = getString(R.string.invalid_host)
return
}
if (!Hosts.isValidIp(ipString)) {
val ipInputLayout =
dialog.findViewById<TextInputLayout>(R.id.ip_input_layout) ?: return
ipInputLayout.error = getString(R.string.invalid_ip)
return
}
hostsFragment!!.hosts!!.put(hostString, ipString)
hostsFragment!!.updateView(true)
dialog.dismiss()
}
protected fun delete(dialog: AlertDialog) {
val host = dialog.findViewById<TextView>(R.id.host) ?: return
val hostString = host.text.toString().trim { it <= ' ' }.lowercase()
hostsFragment!!.hosts!!.delete(hostString)
hostsFragment!!.updateView(true)
dialog.dismiss()
}
}
class AddHostDialogFragment : HostDialogFragment() {
override fun onCreateDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(R.string.add_host)
builder.setPositiveButton(R.string.add_host_add, null)
builder.setNegativeButton(android.R.string.cancel, null)
}
override fun onCreateDialog(dialog: AlertDialog) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener { put(dialog) }
}
}
class EditHostDialogFragment : HostDialogFragment() {
override fun onCreateDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(R.string.edit_host)
builder.setPositiveButton(R.string.edit_host_confirm, null)
builder.setNegativeButton(R.string.edit_host_delete, null)
}
override fun onCreateDialog(dialog: AlertDialog) {
val hostInputLayout =
dialog.findViewById<TextInputLayout>(R.id.host_input_layout) ?: return
hostInputLayout.isEnabled = false
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener { put(dialog) }
dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
.setOnClickListener { delete(dialog) }
}
}
private class HostsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val host: TextView
val ip: TextView
init {
host = itemView.findViewById(R.id.host)
ip = itemView.findViewById(R.id.ip)
}
}
private inner class HostsAdapter : RecyclerView.Adapter<HostsHolder>() {
private val inflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HostsHolder {
return HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false))
}
override fun onBindViewHolder(holder: HostsHolder, position: Int) {
val pair = data!![position]
holder.host.text = pair.first
holder.ip.text = pair.second
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return data!!.size
}
}
companion object {
private val DIALOG_TAG_ADD_HOST = AddHostDialogFragment::class.java.name
private val DIALOG_TAG_EDIT_HOST = EditHostDialogFragment::class.java.name
private const val KEY_HOST = "com.hippo.ehviewer.ui.fragment.HostsFragment.HOST"
private const val KEY_IP = "com.hippo.ehviewer.ui.fragment.HostsFragment.IP"
}
}
@@ -19,29 +19,55 @@
package com.hippo.ehviewer.ui.fragment
import android.os.Bundle
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.SecurityActivity.Companion.isAuthenticationSupported
import com.hippo.ehviewer.ui.isAuthenticationSupported
/**
* Created by Mo10 on 2018/2/10.
*/
class PrivacyFragment : BasePreferenceFragment() {
private lateinit var requireUnlock: SwitchPreferenceCompat
private lateinit var unlockDelay: SeekBarPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.privacy_settings)
requireUnlock = findPreference("require_unlock")!!
unlockDelay = findPreference("require_unlock_delay")!!
requireUnlock.setOnPreferenceChangeListener { _, newValue ->
unlockDelay.isEnabled = newValue as Boolean
setUnlockDelaySummary(unlockDelay.value)
true
}
unlockDelay.setOnPreferenceChangeListener { _, newValue ->
setUnlockDelaySummary(newValue as Int)
true
}
}
override fun onStart() {
super.onStart()
if (!isAuthenticationSupported(requireContext())) {
if (!requireContext().isAuthenticationSupported()) {
Settings.putSecurity(false)
findPreference<SwitchPreferenceCompat>("require_unlock")?.isEnabled = false
findPreference<SwitchPreferenceCompat>("require_unlock")?.isChecked = false
requireUnlock.isEnabled = false
requireUnlock.isChecked = false
unlockDelay.isEnabled = false
} else {
unlockDelay.isEnabled = requireUnlock.isChecked == true
setUnlockDelaySummary(unlockDelay.value)
}
}
override fun getFragmentTitle(): Int {
return R.string.settings_privacy
}
private fun setUnlockDelaySummary(value: Int) {
unlockDelay.summary = if (value == 0) {
getString(R.string.settings_privacy_require_unlock_delay_summary_immediately)
} else {
getString(R.string.settings_privacy_require_unlock_delay_summary, value.toString())
}
}
}
@@ -56,7 +56,6 @@ public abstract class BaseScene extends SceneFragment {
private View drawerView;
@Nullable
private SparseArray<Parcelable> drawerViewState;
private boolean needWhiteStatusBar = needWhiteStatusBar();
public void addAboveSnackView(View view) {
FragmentActivity activity = getActivity();
@@ -118,10 +117,6 @@ public abstract class BaseScene extends SceneFragment {
return true;
}
public boolean needWhiteStatusBar() {
return true;
}
public int getNavCheckedItem() {
return 0;
}
@@ -176,14 +171,6 @@ public abstract class BaseScene extends SceneFragment {
drawerView = null;
}
public void setLightStatusBar(boolean set) {
var insetsController = getInsetsController();
if (insetsController != null) {
insetsController.setAppearanceLightStatusBars(set && (requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) <= 0);
}
needWhiteStatusBar = set;
}
public void onDestroyDrawerView() {
}
@@ -213,7 +200,11 @@ public abstract class BaseScene extends SceneFragment {
// Hide soft ime
hideSoftInput();
setLightStatusBar(needWhiteStatusBar);
insetsController = getInsetsController();
if (insetsController != null) {
insetsController.setAppearanceLightStatusBars((requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) <= 0);
}
}
@Nullable
@@ -0,0 +1,21 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.hippo.ehviewer.R
abstract class CollapsingToolbarScene : ToolbarScene() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view =
inflater.inflate(R.layout.scene_collapsing_toolbar, container, false) as ViewGroup
mToolbar = view.findViewById(R.id.toolbar)
mAppBarLayout = view.findViewById(R.id.appbar)
val contentView = onCreateViewWithToolbar(inflater, view, savedInstanceState)
return view.apply { addView(contentView, 0) }
}
}
@@ -31,14 +31,14 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
@@ -217,7 +217,7 @@ public class CookieSignInScene extends SolidScene implements EditText.OnEditorAc
hideSoftInput();
if (!checkIpbMemberId(ipbMemberId) || !(checkIpbPassHash(ipbPassHash))) {
new MaterialAlertDialogBuilder(context).setTitle(R.string.waring)
new BaseDialogBuilder(context).setTitle(R.string.waring)
.setMessage(R.string.wrong_cookie_warning)
.setPositiveButton(R.string.i_will_check_it, null)
.setNegativeButton(R.string.i_dont_think_so, (dialog, which) -> {
@@ -253,7 +253,7 @@ public class CookieSignInScene extends SolidScene implements EditText.OnEditorAc
private void fillCookiesFromClipboard() {
hideSoftInput();
String text = ClipboardUtil.getTextFromClipboard();
String text = ClipboardUtilKt.getTextFromClipboard(ClipboardUtilKt.getClipboardManager(getContext()), getContext());
if (text == null) {
showTip(R.string.from_clipboard_error, LENGTH_SHORT);
return;
@@ -22,21 +22,17 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
@@ -45,28 +41,20 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.conaco.DataContainer;
import com.hippo.conaco.ProgressNotifier;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.HandlerDrawable;
@@ -83,11 +71,8 @@ import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.download.DownloadService;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.io.UniFileInputStreamPipe;
import com.hippo.scene.Announcer;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.unifile.UniFile;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.view.ViewTransition;
@@ -96,15 +81,10 @@ import com.hippo.widget.LoadImageView;
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.LongList;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -331,6 +311,7 @@ public class DownloadsScene extends ToolbarScene
View content = ViewUtils.$$(view, R.id.content);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(content, R.id.recycler_view);
setLiftOnScrollTargetView(mRecyclerView);
mFastScroller = (FastScroller) ViewUtils.$$(content, R.id.fast_scroller);
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
@@ -437,7 +418,7 @@ public class DownloadsScene extends ToolbarScene
int id = item.getItemId();
if (id == R.id.action_filter) {
new MaterialAlertDialogBuilder(requireActivity())
new BaseDialogBuilder(requireActivity())
.setSingleChoiceItems(R.array.download_state, mType + 1, (dialog, which) -> {
mType = which - 1;
updateForLabel();
@@ -460,7 +441,7 @@ public class DownloadsScene extends ToolbarScene
openDrawer(Gravity.RIGHT);
return true;
} else if (id == R.id.action_reset_reading_progress) {
new MaterialAlertDialogBuilder(getContext())
new BaseDialogBuilder(getContext())
.setMessage(R.string.reset_reading_progress_message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
@@ -533,7 +514,7 @@ public class DownloadsScene extends ToolbarScene
for (int i = 0, n = list.size(); i < n; i++) {
items[i + 2] = list.get(i).getLabel();
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.default_download_label)
.setItems(items, (dialog1, which) -> {
if (which == 0) {
@@ -556,20 +537,13 @@ public class DownloadsScene extends ToolbarScene
initLabels();
RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager();
dragDropManager.setInitiateOnLongPress(true);
dragDropManager.setInitiateOnTouch(false);
dragDropManager.setDraggingItemAlpha(0.8f);
mLabelAdapter = new DownloadLabelAdapter(inflater);
final EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
mLabelAdapter.setHasStableIds(true);
final GeneralItemAnimator animator = new DraggableItemAnimator();
recyclerView.setItemAnimator(animator);
recyclerView.setAdapter(dragDropManager.createWrappedAdapter(mLabelAdapter));
dragDropManager.attachRecyclerView(recyclerView);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new DownloadLabelItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView);
recyclerView.setAdapter(mLabelAdapter);
return view;
}
@@ -727,7 +701,7 @@ public class DownloadsScene extends ToolbarScene
MoveDialogHelper helper = new MoveDialogHelper(labels, downloadInfoList);
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show();
@@ -892,7 +866,7 @@ public class DownloadsScene extends ToolbarScene
holder.speed.setText(FileUtils.humanReadableByteCount(speed, false) + "/S");
}
private static class DownloadLabelHolder extends AbstractDraggableItemViewHolder {
private static class DownloadLabelHolder extends RecyclerView.ViewHolder {
private final TextView label;
private final ImageView option;
@@ -904,74 +878,7 @@ public class DownloadsScene extends ToolbarScene
}
}
private static class ThumbDataContainer implements DataContainer {
private final DownloadInfo mInfo;
@Nullable
private UniFile mFile;
public ThumbDataContainer(@NonNull DownloadInfo info) {
mInfo = info;
}
private void ensureFile() {
if (mFile == null) {
UniFile dir = SpiderDen.getGalleryDownloadDir(mInfo);
if (dir != null && dir.isDirectory()) {
mFile = dir.createFile(".thumb");
}
}
}
@Override
public boolean isEnabled() {
ensureFile();
return mFile != null;
}
@Override
public void onUrlMoved(String requestUrl, String responseUrl) {
}
@Override
public boolean save(InputStream is, long length, String mediaType, ProgressNotifier notify) {
ensureFile();
if (mFile == null) {
return false;
}
OutputStream os = null;
try {
os = mFile.openOutputStream();
IOUtils.copy(is, os);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
IOUtils.closeQuietly(os);
}
}
@Override
public InputStreamPipe get() {
ensureFile();
if (mFile != null) {
return new UniFileInputStreamPipe(mFile);
} else {
return null;
}
}
@Override
public void remove() {
if (mFile != null) {
mFile.delete();
}
}
}
private class DownloadLabelAdapter extends RecyclerView.Adapter<DownloadLabelHolder> implements DraggableItemAdapter<DownloadLabelHolder> {
private class DownloadLabelAdapter extends RecyclerView.Adapter<DownloadLabelHolder> {
private final LayoutInflater mInflater;
@@ -1025,36 +932,15 @@ public class DownloadsScene extends ToolbarScene
});
if (position > 0) {
holder.option.setVisibility(View.VISIBLE);
holder.itemView.setOnLongClickListener(v -> {
holder.option.setOnClickListener(v -> {
if (context != null) {
PopupMenu popupMenu = new PopupMenu(context, holder.option);
popupMenu.inflate(R.menu.download_label_option);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.menu_label_rename) {
EditTextDialogBuilder builder = new EditTextDialogBuilder(
context, label, getString(R.string.download_labels));
builder.setTitle(R.string.rename_label_title);
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.show();
new RenameLabelDialogHelper(builder, dialog, label);
} else if (itemId == R.id.menu_label_remove) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_label_title))
.setMessage(getString(R.string.delete_label_message, label))
.setPositiveButton(R.string.delete, (dialog, which) -> {
mDownloadManager.deleteLabel(label);
mLabels.remove(position);
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
return false;
});
EditTextDialogBuilder builder = new EditTextDialogBuilder(
context, label, getString(R.string.download_labels));
builder.setTitle(R.string.rename_label_title);
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.show();
new RenameLabelDialogHelper(builder, dialog, label);
}
return true;
});
} else {
holder.option.setVisibility(View.GONE);
@@ -1071,43 +957,6 @@ public class DownloadsScene extends ToolbarScene
public int getItemCount() {
return mLabels != null ? mLabels.size() : 0;
}
@Override
public boolean onCheckCanStartDrag(@NonNull DownloadLabelHolder holder, int position, int x, int y) {
return position != 0 && x > holder.option.getX() && y > holder.option.getY();
}
@Override
public ItemDraggableRange onGetItemDraggableRange(@NonNull DownloadLabelHolder holder, int position) {
return new ItemDraggableRange(1, getItemCount() - 1);
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
Context context = getContext();
if (null == context || fromPosition == toPosition || toPosition == 0) {
return;
}
EhApplication.getDownloadManager(context).moveLabel(fromPosition - 1, toPosition - 1);
final String item = mLabels.remove(fromPosition);
mLabels.add(toPosition, item);
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return dropPosition != 0;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
}
private class DeleteRangeDialogHelper implements DialogInterface.OnClickListener {
@@ -1307,8 +1156,7 @@ public class DownloadsScene extends ToolbarScene
return;
}
DownloadInfo info = mList.get(position);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb,
new ThumbDataContainer(info), true);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb, true);
holder.title.setText(EhUtils.getSuitableTitle(info));
holder.uploader.setText(info.uploader);
holder.rating.setRating(info.rating);
@@ -1445,4 +1293,43 @@ public class DownloadsScene extends ToolbarScene
}
}
}
private class DownloadLabelItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT);
}
@SuppressLint("NotifyDataSetChanged")
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
Context context = getContext();
if (null == context || fromPosition == toPosition || toPosition == 0 || fromPosition == 0) {
return false;
}
EhApplication.getDownloadManager(context).moveLabel(fromPosition - 1, toPosition - 1);
final String item = mLabels.remove(fromPosition);
mLabels.add(toPosition, item);
mLabelAdapter.notifyDataSetChanged();
return true;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getBindingAdapterPosition();
if (mDownloadManager == null)
return;
if (position != 0) {
String label = mLabels.get(position);
mDownloadManager.deleteLabel(label);
}
mLabels.remove(position);
mLabelAdapter.notifyDataSetChanged();
}
}
}
@@ -21,13 +21,10 @@ import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.InputType;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -36,7 +33,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
@@ -45,10 +41,14 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.datepicker.CalendarConstraints;
import com.google.android.material.datepicker.CompositeDateValidator;
import com.google.android.material.datepicker.DateValidatorPointBackward;
import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.annotation.Implemented;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
@@ -77,11 +77,13 @@ import com.hippo.widget.ContentLayout;
import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.ViewUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -116,19 +118,6 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@ViewLifeCircle
private FabLayout mFabLayout;
private final Runnable showNormalFabsRunnable = new Runnable() {
@Override
public void run() {
if (mFabLayout != null) {
mFabLayout.setSecondaryFabVisibilityAt(0, true);
mFabLayout.setSecondaryFabVisibilityAt(1, true);
mFabLayout.setSecondaryFabVisibilityAt(2, true);
mFabLayout.setSecondaryFabVisibilityAt(3, false);
mFabLayout.setSecondaryFabVisibilityAt(4, false);
mFabLayout.setSecondaryFabVisibilityAt(5, false);
}
}
};
@Nullable
@ViewLifeCircle
private FavoritesAdapter mAdapter;
@@ -159,6 +148,21 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@WholeLifeCircle
private FavListUrlBuilder mUrlBuilder;
private final Runnable showNormalFabsRunnable = new Runnable() {
@Override
public void run() {
if (mFabLayout != null) {
mFabLayout.setSecondaryFabVisibilityAt(0, true);
mFabLayout.setSecondaryFabVisibilityAt(1, true);
mFabLayout.setSecondaryFabVisibilityAt(2, true);
mFabLayout.setSecondaryFabVisibilityAt(3, false);
mFabLayout.setSecondaryFabVisibilityAt(4, false);
mFabLayout.setSecondaryFabVisibilityAt(5, false);
mFabLayout.setSecondaryFabVisibilityAt(6, false);
}
updateJumpFab();
}
};
private int mFavLocalCount = 0;
private int mFavCountSum = 0;
private boolean mHasFirstRefresh;
@@ -285,6 +289,7 @@ public class FavoritesScene extends BaseScene implements
mSearchBar.setHelper(this);
mSearchBar.setAllowEmptySearch(false);
updateSearchBar();
updateJumpFab();
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView);
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
@@ -353,6 +358,13 @@ public class FavoritesScene extends BaseScene implements
Settings.putRecentFavCat(mUrlBuilder.getFavCat());
}
// Hide jump fab on local fav cat
private void updateJumpFab() {
if (mFabLayout != null && mUrlBuilder != null) {
mFabLayout.setSecondaryFabVisibilityAt(1, mUrlBuilder.getFavCat() != FavListUrlBuilder.FAV_CAT_LOCAL);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -402,7 +414,7 @@ public class FavoritesScene extends BaseScene implements
items[1] = getString(R.string.local_favorites);
String[] favCat = Settings.getFavCat();
System.arraycopy(favCat, 0, items, 2, 10);
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.default_favorites_collection)
.setItems(items, (dialog, which) -> Settings.putDefaultFavSlot(which - 2)).show();
return true;
@@ -488,6 +500,7 @@ public class FavoritesScene extends BaseScene implements
mUrlBuilder.setKeyword(null);
mUrlBuilder.setFavCat(newFavCat);
updateSearchBar();
updateJumpFab();
mHelper.refresh();
closeDrawer(Gravity.RIGHT);
@@ -648,35 +661,27 @@ public class FavoritesScene extends BaseScene implements
return;
}
final int page = mHelper.getPageForTop();
final int pages = mHelper.getPages();
String hint = getString(R.string.go_to_hint, page + 1, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
LocalDateTime local = LocalDateTime.of(2007, 3, 21, 0, 0);
long fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
long toDate = MaterialDatePicker.todayInUtcMilliseconds();
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goTo(goTo);
dialog.dismiss();
ArrayList listValidators = new ArrayList<>();
listValidators.add(DateValidatorPointForward.from(fromDate));
listValidators.add(DateValidatorPointBackward.before(toDate));
var constraintsBuilder = new CalendarConstraints.Builder()
.setStart(fromDate)
.setEnd(toDate)
.setValidator(CompositeDateValidator.allOf(listValidators));
var datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(constraintsBuilder.build())
.setTitleText(R.string.go_to)
.setSelection(toDate)
.build();
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
datePicker.addOnPositiveButtonClickListener(v -> {
mHelper.goTo(v);
});
}
@@ -718,7 +723,11 @@ public class FavoritesScene extends BaseScene implements
}
switch (position) {
case 3: { // Download
case 3: { // Check all
mRecyclerView.checkAll();
break;
}
case 4: { // Download
Activity activity = getMainActivity();
if (activity != null) {
CommonOperations.startDownload(getMainActivity(), mModifyGiList, false);
@@ -729,9 +738,9 @@ public class FavoritesScene extends BaseScene implements
}
break;
}
case 4: { // Delete
case 5: { // Delete
DeleteDialogHelper helper = new DeleteDialogHelper();
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.delete_favorites_dialog_title)
.setMessage(getString(R.string.delete_favorites_dialog_message, mModifyGiList.size()))
.setPositiveButton(android.R.string.ok, helper)
@@ -739,13 +748,13 @@ public class FavoritesScene extends BaseScene implements
.show();
break;
}
case 5: { // Move
case 6: { // Move
MoveDialogHelper helper = new MoveDialogHelper();
// First is local favorite, the other 10 is cloud favorite
String[] array = new String[11];
array[0] = getString(R.string.local_favorites);
System.arraycopy(Settings.getFavCat(), 0, array, 1, 10);
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.move_favorites_dialog_title)
.setItems(array, helper)
.setOnCancelListener(helper)
@@ -771,6 +780,7 @@ public class FavoritesScene extends BaseScene implements
mFabLayout.setSecondaryFabVisibilityAt(3, true);
mFabLayout.setSecondaryFabVisibilityAt(4, true);
mFabLayout.setSecondaryFabVisibilityAt(5, true);
mFabLayout.setSecondaryFabVisibilityAt(6, true);
}
}
@@ -854,7 +864,29 @@ public class FavoritesScene extends BaseScene implements
}
updateSearchBar();
mHelper.onGetPageData(taskId, result.pages, result.nextPage, result.galleryInfoList);
assert mUrlBuilder != null;
int pages = 0;
if (result.nextPage == null)
pages = mHelper.pgCounter + 1;
else
pages = Integer.MAX_VALUE;
String prev = result.prevPage, next = result.nextPage;
switch (mHelper.operation) {
case -1:
if (prev != null) mHelper.upperPage = prev;
break;
case 1:
if (next != null) mHelper.lowerPage = next;
break;
default:
mHelper.upperPage = prev;
mHelper.lowerPage = next;
break;
}
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
if (mDrawerAdapter != null) {
mDrawerAdapter.notifyDataSetChanged();
@@ -1173,6 +1205,10 @@ public class FavoritesScene extends BaseScene implements
}
private class FavoritesHelper extends GalleryInfoContentHelper {
public String upperPage = null;
public String lowerPage = null;
public int operation = 0;
public int pgCounter = 0;
@Override
protected void getPageData(final int taskId, int type, int page) {
@@ -1180,6 +1216,7 @@ public class FavoritesScene extends BaseScene implements
if (null == activity || null == mUrlBuilder || null == mClient) {
return;
}
pgCounter = page;
if (mEnableModify) {
mEnableModify = false;
@@ -1219,7 +1256,7 @@ public class FavoritesScene extends BaseScene implements
url = mUrlBuilder.build();
}
mUrlBuilder.setIndex(page);
mUrlBuilder.setNext(lowerPage);
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_MODIFY_FAVORITES);
request.setCallback(new GetFavoritesListener(getContext(),
@@ -1232,7 +1269,25 @@ public class FavoritesScene extends BaseScene implements
final String keyword = mUrlBuilder.getKeyword();
SimpleHandler.getInstance().post(() -> onGetFavoritesLocal(keyword, taskId));
} else {
mUrlBuilder.setIndex(page);
String prev = null, next = null;
if (jumpTo != null) {
next = Integer.toString(minGid);
operation = 0;
} else if (page != 0) {
if (page >= mHelper.getPageForTop()) {
next = lowerPage;
operation = 1;
} else {
prev = upperPage;
operation = -1;
}
}
mUrlBuilder.setPrev(prev);
mUrlBuilder.setNext(next);
mUrlBuilder.setJumpTo(jumpTo);
jumpTo = null;
String url = mUrlBuilder.build();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
@@ -1295,5 +1350,34 @@ public class FavoritesScene extends BaseScene implements
}
}
}
@Override
protected void onClearData() {
super.onClearData();
}
@Override
protected void beforeRefresh() {
super.beforeRefresh();
upperPage = null;
lowerPage = null;
operation = 0;
}
@Override
protected Parcelable saveInstanceState(Parcelable superState) {
Bundle bundle = (Bundle) super.saveInstanceState(superState);
bundle.putString(KEY_UPPER_PAGE, upperPage);
bundle.putString(KEY_LOWER_PAGE, lowerPage);
return bundle;
}
@Override
protected Parcelable restoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
upperPage = bundle.getString(KEY_UPPER_PAGE);
lowerPage = bundle.getString(KEY_LOWER_PAGE);
return super.restoreInstanceState(state);
}
}
}
@@ -54,8 +54,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
@@ -72,7 +72,7 @@ import com.hippo.ehviewer.client.parser.VoteCommentParser;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import com.hippo.text.URLImageGetter;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.ReadableTime;
import com.hippo.util.TextUrl;
@@ -170,8 +170,9 @@ public final class GalleryCommentsScene extends ToolbarScene
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_comments, container, false);
ViewGroup view = (ViewGroup) inflater.inflate(R.layout.scene_gallery_comments, container, false);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
setLiftOnScrollTargetView(mRecyclerView);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mEditPanel = ViewUtils.$$(view, R.id.edit_panel);
mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send);
@@ -179,19 +180,21 @@ public final class GalleryCommentsScene extends ToolbarScene
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
mFab = (FloatingActionButton) ViewUtils.$$(view, R.id.fab);
mRefreshLayout = (SwipeRefreshLayout) ViewUtils.$$(view, R.id.refresh_layout);
// Workaround for fab and edittext render out of screen
view.removeView(mFabLayout);
view.removeView((View) mEditPanel.getParent());
assert container != null;
container.addView(mFabLayout);
container.addView((View) mEditPanel.getParent());
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mEditPanel,
mFabLayout
));
mRefreshLayout.setColorSchemeResources(
R.color.loading_indicator_red,
R.color.loading_indicator_purple,
R.color.loading_indicator_blue,
R.color.loading_indicator_cyan,
R.color.loading_indicator_green,
R.color.loading_indicator_yellow);
mRefreshLayout.setColorSchemeColors(ResourcesKt.resolveColor(getTheme(), androidx.appcompat.R.attr.colorPrimary));
mRefreshLayout.setOnRefreshListener(this);
Context context = requireContext();
@@ -297,7 +300,7 @@ public final class GalleryCommentsScene extends ToolbarScene
}
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
BaseDialogBuilder builder = new BaseDialogBuilder(context);
context = builder.getContext();
final LayoutInflater inflater = LayoutInflater.from(context);
EasyRecyclerView rv = (EasyRecyclerView) inflater.inflate(R.layout.dialog_recycler_view, null);
@@ -354,15 +357,14 @@ public final class GalleryCommentsScene extends ToolbarScene
menuId.add(R.id.check_vote_status);
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setItems(menu.toArray(new String[0]), (dialog, which) -> {
if (which < 0 || which >= menuId.size()) {
return;
}
int id = menuId.get(which);
if (id == R.id.copy) {
ClipboardUtil.addTextToClipboard(comment.comment);
showTip(R.string.copied_to_clipboard, LENGTH_SHORT);
ClipboardUtilKt.addTextToClipboard(requireActivity(), comment.comment, false);
} else if (id == R.id.vote_up) {
voteComment(comment.id, 1);
} else if (id == R.id.vote_down) {
@@ -385,8 +387,7 @@ public final class GalleryCommentsScene extends ToolbarScene
}
RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
if (holder instanceof ActualCommentHolder) {
ActualCommentHolder commentHolder = (ActualCommentHolder) holder;
if (holder instanceof ActualCommentHolder commentHolder) {
ClickableSpan span = commentHolder.comment.getCurrentSpan();
commentHolder.comment.clearCurrentSpan();
@@ -31,6 +31,7 @@ import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
@@ -40,20 +41,16 @@ import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RatingBar;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -61,10 +58,12 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.widget.NestedScrollView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.ehviewer.AppConfig;
@@ -94,17 +93,21 @@ import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.gallery.EhGalleryProvider;
import com.hippo.ehviewer.gallery.GalleryProvider2;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.annotation.WholeLifeCircle;
import com.hippo.ehviewer.widget.GalleryRatingBar;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.text.URLImageGetter;
import com.hippo.unifile.UniFile;
import com.hippo.util.AppHelper;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.util.ReadableTime;
import com.hippo.view.ViewTransition;
import com.hippo.widget.AutoWrapLayout;
@@ -115,7 +118,6 @@ import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.IntList;
@@ -131,10 +133,9 @@ import java.util.List;
import okhttp3.HttpUrl;
import rikka.core.res.ResourcesKt;
public class GalleryDetailScene extends ToolbarScene implements View.OnClickListener,
public class GalleryDetailScene extends CollapsingToolbarScene implements View.OnClickListener,
com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener,
View.OnLongClickListener {
public final static String KEY_ACTION = "action";
public static final String ACTION_GALLERY_INFO = "action_gallery_info";
public static final String ACTION_GID_TOKEN = "action_gid_token";
@@ -160,14 +161,10 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
private ViewTransition mViewTransition;
// Header
@Nullable
private FrameLayout mHeader;
@Nullable
private View mColorBg;
private LinearLayout mHeader;
@Nullable
private LoadImageView mThumb;
@Nullable
private TextView mTitle;
@Nullable
private TextView mUploader;
@Nullable
private TextView mCategory;
@@ -203,8 +200,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
@Nullable
private RatingBar mRating;
@Nullable
private View mHeartGroup;
@Nullable
private TextView mHeart;
@Nullable
private TextView mHeartOutline;
@@ -260,7 +255,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
result -> {
if (result && mGalleryDetail != null) {
TorrentListDialogHelper helper = new TorrentListDialogHelper();
Dialog dialog = new MaterialAlertDialogBuilder(requireActivity())
Dialog dialog = new BaseDialogBuilder(requireActivity())
.setTitle(R.string.torrents)
.setView(R.layout.dialog_torrent_list)
.setOnDismissListener(helper)
@@ -287,6 +282,21 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return null;
}
private static void deleteFileAsync(UniFile... files) {
//noinspection deprecation
new AsyncTask<UniFile, Void, Void>() {
@Override
protected Void doInBackground(UniFile... params) {
for (UniFile file : params) {
if (file != null) {
file.delete();
}
}
return null;
}
}.executeOnExecutor(IoThreadPoolExecutor.getInstance(), files);
}
@StringRes
private int getRatingText(float rating) {
int resId;
@@ -558,13 +568,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
View view = inflater.inflate(R.layout.scene_gallery_detail, container, false);
ViewGroup main = (ViewGroup) ViewUtils.$$(view, R.id.main);
ScrollView mainView = (ScrollView) ViewUtils.$$(main, R.id.scroll_view);
mainView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
if (mActionGroup != null && mHeader != null) {
//noinspection IntegerDivisionInFloatingPointContext
setLightStatusBar(mActionGroup.getY() - mHeader.findViewById(R.id.header_content).getPaddingTop() / 2 < scrollY);
}
});
NestedScrollView mainView = (NestedScrollView) ViewUtils.$$(main, R.id.scroll_view);
setLiftOnScrollTargetView(mainView);
View progressView = ViewUtils.$$(main, R.id.progress_view);
mTip = (TextView) ViewUtils.$$(main, R.id.tip);
mViewTransition = new ViewTransition(mainView, progressView, mTip);
@@ -577,10 +582,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mTip.setCompoundDrawables(null, drawable, null, null);
mTip.setOnClickListener(this);
mHeader = (FrameLayout) ViewUtils.$$(mainView, R.id.header);
mColorBg = ViewUtils.$$(mHeader, R.id.color_bg);
mHeader = (LinearLayout) ViewUtils.$$(mainView, R.id.header);
mThumb = (LoadImageView) ViewUtils.$$(mHeader, R.id.thumb);
mTitle = (TextView) ViewUtils.$$(mHeader, R.id.title);
mUploader = (TextView) ViewUtils.$$(mHeader, R.id.uploader);
mCategory = (TextView) ViewUtils.$$(mHeader, R.id.category);
mActionGroup = (ViewGroup) ViewUtils.$$(mHeader, R.id.action_card);
@@ -610,9 +613,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mNewerVersion = (TextView) ViewUtils.$$(mActions, R.id.newerVersion);
mRatingText = (TextView) ViewUtils.$$(mActions, R.id.rating_text);
mRating = (RatingBar) ViewUtils.$$(mActions, R.id.rating);
mHeartGroup = ViewUtils.$$(mActions, R.id.heart_group);
mHeart = (TextView) ViewUtils.$$(mHeartGroup, R.id.heart);
mHeartOutline = (TextView) ViewUtils.$$(mHeartGroup, R.id.heart_outline);
mHeart = (TextView) ViewUtils.$$(mActions, R.id.heart);
mHeartOutline = (TextView) ViewUtils.$$(mActions, R.id.heart_outline);
mTorrent = (TextView) ViewUtils.$$(mActions, R.id.torrent);
mArchive = (TextView) ViewUtils.$$(mActions, R.id.archive);
mShare = (TextView) ViewUtils.$$(mActions, R.id.share);
@@ -620,15 +622,16 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mSimilar = (TextView) ViewUtils.$$(mActions, R.id.similar);
mSearchCover = (TextView) ViewUtils.$$(mActions, R.id.search_cover);
mNewerVersion.setOnClickListener(this);
mHeartGroup.setOnClickListener(this);
mHeartGroup.setOnLongClickListener(this);
mHeart.setOnClickListener(this);
mHeart.setOnLongClickListener(this);
mHeartOutline.setOnClickListener(this);
mHeartOutline.setOnLongClickListener(this);
mTorrent.setOnClickListener(this);
mArchive.setOnClickListener(this);
mShare.setOnClickListener(this);
mRate.setOnClickListener(this);
mSimilar.setOnClickListener(this);
mSearchCover.setOnClickListener(this);
ensureActionDrawable();
mTags = (LinearLayout) ViewUtils.$$(belowHeader, R.id.tags);
mNoTags = (TextView) ViewUtils.$$(mTags, R.id.no_tags);
@@ -649,11 +652,9 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
if (prepareData()) {
if (mGalleryDetail != null) {
bindViewSecond();
setTransitionName();
adjustViewVisibility(STATE_NORMAL, false);
} else if (mGalleryInfo != null) {
bindViewFirst();
setTransitionName();
adjustViewVisibility(STATE_REFRESH_HEADER, false);
} else {
adjustViewVisibility(STATE_REFRESH, false);
@@ -668,11 +669,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return view;
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -685,9 +681,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mViewTransition = null;
mHeader = null;
mColorBg = null;
mThumb = null;
mTitle = null;
mUploader = null;
mCategory = null;
mActionGroup = null;
@@ -706,7 +700,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mNewerVersion = null;
mRatingText = null;
mRating = null;
mHeartGroup = null;
mHeart = null;
mHeartOutline = null;
mTorrent = null;
@@ -780,52 +773,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return true;
}
private void setActionDrawable(@Nullable TextView text, @DrawableRes int resId) {
if (text == null) return;
Context context = text.getContext();
Drawable drawable = ContextCompat.getDrawable(context, resId);
if (drawable == null) return;
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
text.setCompoundDrawables(null, drawable, null, null);
}
private void ensureActionDrawable() {
setActionDrawable(mHeart, R.drawable.v_heart_primary_x48);
setActionDrawable(mHeartOutline, R.drawable.v_heart_outline_primary_x48);
setActionDrawable(mTorrent, R.drawable.v_utorrent_primary_x48);
setActionDrawable(mArchive, R.drawable.v_archive_primary_x48);
setActionDrawable(mShare, R.drawable.v_share_primary_x48);
setActionDrawable(mSimilar, R.drawable.v_similar_primary_x48);
setActionDrawable(mSearchCover, R.drawable.v_file_find_primary_x48);
}
private boolean createCircularReveal() {
if (mColorBg == null) {
return false;
}
int w = mColorBg.getWidth();
int h = mColorBg.getHeight();
if (ViewCompat.isAttachedToWindow(mColorBg) && w != 0 && h != 0) {
Resources resources = getContext().getResources();
int keylineMargin = resources.getDimensionPixelSize(R.dimen.keyline_margin);
int thumbWidth = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_width);
int thumbHeight = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_height);
int x = thumbWidth / 2 + keylineMargin;
int y = thumbHeight / 2 + keylineMargin;
int radiusX = Math.max(Math.abs(x), Math.abs(w - x));
int radiusY = Math.max(Math.abs(y), Math.abs(h - y));
float radius = (float) Math.hypot(radiusX, radiusY);
ViewAnimationUtils.createCircularReveal(mColorBg, x, y, 0, radius).setDuration(300).start();
return true;
} else {
return false;
}
}
private void adjustViewVisibility(int state, boolean animation) {
if (state == mState) {
return;
@@ -841,19 +788,16 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
switch (state) {
case STATE_NORMAL:
setLightStatusBar(false);
// Show mMainView
mViewTransition.showView(0, animation);
// Show mBelowHeader
mViewTransition2.showView(0, animation);
break;
case STATE_REFRESH:
setLightStatusBar(true);
// Show mProgressView
mViewTransition.showView(1, animation);
break;
case STATE_REFRESH_HEADER:
setLightStatusBar(false);
// Show mMainView
mViewTransition.showView(0, animation);
// Show mProgress
@@ -862,32 +806,24 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
default:
case STATE_INIT:
case STATE_FAILED:
setLightStatusBar(true);
// Show mFailedView
mViewTransition.showView(2, animation);
break;
}
if ((oldState == STATE_INIT || oldState == STATE_FAILED || oldState == STATE_REFRESH) &&
(state == STATE_NORMAL || state == STATE_REFRESH_HEADER) && ResourcesKt.resolveBoolean(getTheme(), androidx.appcompat.R.attr.isLightTheme, false)) {
if (!createCircularReveal()) {
SimpleHandler.getInstance().post(this::createCircularReveal);
}
}
}
private void bindViewFirst() {
if (mGalleryDetail != null) {
return;
}
if (mThumb == null || mTitle == null || mUploader == null || mCategory == null) {
if (mThumb == null || mUploader == null || mCategory == null) {
return;
}
if (ACTION_GALLERY_INFO.equals(mAction) && mGalleryInfo != null) {
GalleryInfo gi = mGalleryInfo;
mThumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
mTitle.setText(EhUtils.getSuitableTitle(gi));
setTitle(EhUtils.getSuitableTitle(gi));
mUploader.setText(gi.uploader);
mUploader.setAlpha(gi.disowned ? .5f : 1f);
mCategory.setText(EhUtils.getCategory(gi.category));
@@ -935,7 +871,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
})
.show();
}
if (mThumb == null || mTitle == null || mUploader == null || mCategory == null ||
if (mThumb == null || mUploader == null || mCategory == null ||
mLanguage == null || mPages == null || mSize == null || mPosted == null ||
mFavoredTimes == null || mRatingText == null || mRating == null || mTorrent == null || mNewerVersion == null) {
return;
@@ -944,7 +880,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
Resources resources = getResources();
mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb);
mTitle.setText(EhUtils.getSuitableTitle(gd));
setTitle(EhUtils.getSuitableTitle(gd));
mUploader.setText(gd.uploader);
mUploader.setAlpha(gd.disowned ? .5f : 1f);
mCategory.setText(EhUtils.getCategory(gd.category));
@@ -1112,10 +1048,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
private void setTransitionName() {
long gid = getGid();
if (gid != -1 && mThumb != null &&
mTitle != null && mUploader != null && mCategory != null) {
if (gid != -1 && mThumb != null && mUploader != null && mCategory != null) {
ViewCompat.setTransitionName(mThumb, TransitionNameFactory.getThumbTransitionName(gid));
ViewCompat.setTransitionName(mTitle, TransitionNameFactory.getTitleTransitionName(gid));
ViewCompat.setTransitionName(mUploader, TransitionNameFactory.getUploaderTransitionName(gid));
ViewCompat.setTransitionName(mCategory, TransitionNameFactory.getCategoryTransitionName(gid));
}
@@ -1173,7 +1107,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
lub.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
lub.setImagePath(temp.getPath());
lub.setUseSimilarityScan(true);
lub.setShowExpunged(true);
GalleryListScene.startScene(this, lub);
}
} catch (FileNotFoundException e) {
@@ -1218,10 +1151,14 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
if (EhApplication.getDownloadManager(context).getDownloadState(galleryInfo.gid) == DownloadInfo.STATE_INVALID) {
CommonOperations.startDownload(activity, galleryInfo, false);
} else {
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(getString(R.string.download_remove_dialog_message, galleryInfo.title))
.setPositiveButton(android.R.string.ok, (dialog1, which1) -> EhApplication.getDownloadManager(context).deleteDownload(galleryInfo.gid))
CheckBoxDialogBuilder builder = new CheckBoxDialogBuilder(context,
getString(R.string.download_remove_dialog_message, galleryInfo.title),
getString(R.string.download_remove_dialog_check_text),
Settings.getRemoveImageFiles());
DeleteDialogHelper helper = new DeleteDialogHelper(
EhApplication.getDownloadManager(context), galleryInfo, builder);
builder.setTitle(R.string.download_remove_dialog_title)
.setPositiveButton(android.R.string.ok, helper)
.show();
}
}
@@ -1244,7 +1181,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
for (GalleryInfo newerVersion : mGalleryDetail.newerVersions) {
titles.add(getString(R.string.newer_version_title, newerVersion.title, newerVersion.posted));
}
new MaterialAlertDialogBuilder(requireContext())
new BaseDialogBuilder(requireContext())
.setItems(titles.toArray(new CharSequence[0]), (dialog, which) -> {
GalleryInfo newerVersion = mGalleryDetail.newerVersions.get(which);
Bundle args = new Bundle();
@@ -1256,10 +1193,10 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
.show();
}
} else if (mInfo == v) {
Bundle args = new Bundle();
args.putParcelable(GalleryInfoScene.KEY_GALLERY_DETAIL, mGalleryDetail);
startScene(new Announcer(GalleryInfoScene.class).setArgs(args));
} else if (mHeartGroup == v) {
assert mGalleryDetail != null;
var galleryInfoBottomSheet = new GalleryInfoBottomSheet(mGalleryDetail);
galleryInfoBottomSheet.show(requireActivity().getSupportFragmentManager(), GalleryInfoBottomSheet.TAG);
} else if (mHeart == v || mHeartOutline == v) {
if (mGalleryDetail != null && !mModifingFavorites) {
boolean remove = false;
if (EhDB.containLocalFavorites(mGalleryDetail.gid) || mGalleryDetail.isFavorited) {
@@ -1290,7 +1227,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
} else {
TorrentListDialogHelper helper = new TorrentListDialogHelper();
Dialog dialog = new MaterialAlertDialogBuilder(context)
Dialog dialog = new BaseDialogBuilder(context)
.setTitle(R.string.torrents)
.setView(R.layout.dialog_torrent_list)
.setOnDismissListener(helper)
@@ -1307,7 +1244,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return;
}
ArchiveListDialogHelper helper = new ArchiveListDialogHelper();
Dialog dialog = new MaterialAlertDialogBuilder(context)
Dialog dialog = new BaseDialogBuilder(context)
.setTitle(R.string.dialog_archive_title)
.setView(R.layout.dialog_archive_list)
.setOnDismissListener(helper)
@@ -1322,7 +1259,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return;
}
RateDialogHelper helper = new RateDialogHelper();
Dialog dialog = new MaterialAlertDialogBuilder(context)
Dialog dialog = new BaseDialogBuilder(context)
.setTitle(R.string.rate)
.setView(R.layout.dialog_rate)
.setNegativeButton(android.R.string.cancel, null)
@@ -1355,8 +1292,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
}
} else {
Object o = v.getTag(R.id.tag);
if (o instanceof String) {
String tag = (String) o;
if (o instanceof String tag) {
ListUrlBuilder lub = new ListUrlBuilder();
lub.setMode(ListUrlBuilder.MODE_TAG);
lub.setKeyword(tag);
@@ -1384,7 +1320,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return;
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_uploader, uploader))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Filter filter = new Filter();
@@ -1404,7 +1340,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return;
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_tag, tag))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Filter filter = new Filter();
@@ -1453,7 +1389,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
menuId.add(R.id.vote_down);
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(tag)
.setItems(menu.toArray(new String[0]), (dialog, which) -> {
if (which < 0 || which >= menuId.size()) {
@@ -1469,11 +1405,9 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
} else if (id == R.id.add_filter) {
showFilterTagDialog(tag);
} else if (id == R.id.copy) {
ClipboardUtil.addTextToClipboard(tag);
showTip(R.string.copied_to_clipboard, LENGTH_SHORT);
ClipboardUtilKt.addTextToClipboard(requireActivity(), tag, false);
} else if (id == R.id.copy_trans) {
ClipboardUtil.addTextToClipboard(tv.getText().toString());
showTip(R.string.copied_to_clipboard, LENGTH_SHORT);
ClipboardUtilKt.addTextToClipboard(requireActivity(), tv.getText().toString(), false);
}
}).show();
}
@@ -1508,7 +1442,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
CommonOperations.startDownload(activity, galleryInfo, true);
}
return true;
} else if (mHeartGroup == v) {
} else if (mHeart == v || mHeartOutline == v) {
if (mGalleryDetail != null && !mModifingFavorites) {
boolean remove = false;
if (EhDB.containLocalFavorites(mGalleryDetail.gid) || mGalleryDetail.isFavorited) {
@@ -2176,4 +2110,38 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
EhApplication.getEhClient(context).execute(request);
}
}
private class DeleteDialogHelper implements DialogInterface.OnClickListener {
private final com.hippo.ehviewer.download.DownloadManager mDownloadManager;
private final GalleryInfo mGalleryInfo;
private final CheckBoxDialogBuilder mBuilder;
public DeleteDialogHelper(com.hippo.ehviewer.download.DownloadManager downloadManager,
GalleryInfo galleryInfo, CheckBoxDialogBuilder builder) {
mDownloadManager = downloadManager;
mGalleryInfo = galleryInfo;
mBuilder = builder;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which != DialogInterface.BUTTON_POSITIVE) {
return;
}
// Delete
if (null != mDownloadManager) {
mDownloadManager.deleteDownload(mGalleryInfo.gid);
}
// Delete image files
boolean checked = mBuilder.isChecked();
Settings.putRemoveImageFiles(checked);
if (checked) {
EhDB.removeDownloadDirname(mGalleryInfo.gid);
UniFile file = SpiderDen.getGalleryDownloadDir(mGalleryInfo);
deleteFileAsync(file);
}
}
}
}
@@ -1,267 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.UrlOpener;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.util.ClipboardUtil;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
public final class GalleryInfoScene extends ToolbarScene {
public static final String KEY_GALLERY_DETAIL = "gallery_detail";
public static final String KEY_KEYS = "keys";
public static final String KEY_VALUES = "values";
private static final int INDEX_URL = 3;
private static final int INDEX_PARENT = 10;
/*---------------
Whole life cycle
---------------*/
@Nullable
private ArrayList<String> mKeys;
@Nullable
private ArrayList<String> mValues;
@Nullable
private EasyRecyclerView mRecyclerView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void handlerArgs(Bundle args) {
if (args == null) {
return;
}
GalleryDetail gd = args.getParcelable(KEY_GALLERY_DETAIL);
if (gd == null) {
return;
}
if (mKeys == null || mValues == null) {
return;
}
mKeys.add(getString(R.string.header_key));
mValues.add(getString(R.string.header_value));
mKeys.add(getString(R.string.key_gid));
mValues.add(Long.toString(gd.gid));
mKeys.add(getString(R.string.key_token));
mValues.add(gd.token);
mKeys.add(getString(R.string.key_url));
mValues.add(EhUrl.getGalleryDetailUrl(gd.gid, gd.token));
mKeys.add(getString(R.string.key_title));
mValues.add(gd.title);
mKeys.add(getString(R.string.key_title_jpn));
mValues.add(gd.titleJpn);
mKeys.add(getString(R.string.key_thumb));
mValues.add(gd.thumb);
mKeys.add(getString(R.string.key_category));
mValues.add(EhUtils.getCategory(gd.category));
mKeys.add(getString(R.string.key_uploader));
mValues.add(gd.uploader);
mKeys.add(getString(R.string.key_posted));
mValues.add(gd.posted);
mKeys.add(getString(R.string.key_parent));
mValues.add(gd.parent);
mKeys.add(getString(R.string.key_visible));
mValues.add(gd.visible);
mKeys.add(getString(R.string.key_language));
mValues.add(gd.language);
mKeys.add(getString(R.string.key_pages));
mValues.add(Integer.toString(gd.pages));
mKeys.add(getString(R.string.key_size));
mValues.add(gd.size);
mKeys.add(getString(R.string.key_favorite_count));
mValues.add(Integer.toString(gd.favoriteCount));
mKeys.add(getString(R.string.key_favorited));
mValues.add(Boolean.toString(gd.isFavorited));
mKeys.add(getString(R.string.key_rating_count));
mValues.add(Integer.toString(gd.ratingCount));
mKeys.add(getString(R.string.key_rating));
mValues.add(Float.toString(gd.rating));
mKeys.add(getString(R.string.key_torrents));
mValues.add(Integer.toString(gd.torrentCount));
mKeys.add(getString(R.string.key_torrent_url));
mValues.add(gd.torrentUrl);
mKeys.add(getString(R.string.favorite_name));
mValues.add(gd.favoriteName);
}
protected void onInit() {
mKeys = new ArrayList<>();
mValues = new ArrayList<>();
handlerArgs(getArguments());
}
protected void onRestore(@NonNull Bundle savedInstanceState) {
mKeys = savedInstanceState.getStringArrayList(KEY_KEYS);
mValues = savedInstanceState.getStringArrayList(KEY_VALUES);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putStringArrayList(KEY_KEYS, mKeys);
outState.putStringArrayList(KEY_VALUES, mValues);
}
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_info, container, false);
Context context = getContext();
AssertUtils.assertNotNull(context);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
InfoAdapter adapter = new InfoAdapter();
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false));
mRecyclerView.setClipToPadding(false);
mRecyclerView.setHasFixedSize(true);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_info);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
}
public boolean onItemClick(int position) {
Context context = getContext();
if (null != context && 0 != position && null != mValues) {
if (position == INDEX_PARENT) {
UrlOpener.openUrl(context, mValues.get(position), true);
} else {
ClipboardUtil.addTextToClipboard(mValues.get(position));
if (position == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(mValues.get(position).hashCode());
}
showTip(R.string.copied_to_clipboard, LENGTH_SHORT);
}
return true;
} else {
return false;
}
}
@Override
public void onNavigationClick() {
onBackPressed();
}
private static class InfoHolder extends RecyclerView.ViewHolder {
private final TextView key;
private final TextView value;
public InfoHolder(View itemView) {
super(itemView);
key = itemView.findViewById(R.id.key);
value = itemView.findViewById(R.id.value);
}
}
private class InfoAdapter extends RecyclerView.Adapter<InfoHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_DATA = 1;
private final LayoutInflater mInflater;
public InfoAdapter() {
mInflater = getLayoutInflater();
AssertUtils.assertNotNull(mInflater);
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else {
return TYPE_DATA;
}
}
@NonNull
@Override
public InfoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new InfoHolder(mInflater.inflate(viewType == TYPE_HEADER ?
R.layout.item_gallery_info_header : R.layout.item_gallery_info_data, parent, false));
}
@Override
public void onBindViewHolder(@NonNull InfoHolder holder, int position) {
if (mKeys != null && mValues != null) {
holder.key.setText(mKeys.get(position));
holder.value.setText(mValues.get(position));
holder.itemView.setEnabled(position != 0);
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
}
@Override
public int getItemCount() {
return mKeys == null || mValues == null ? 0 : Math.min(mKeys.size(), mValues.size());
}
}
}
@@ -18,13 +18,10 @@ package com.hippo.ehviewer.ui.scene;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
@@ -33,10 +30,8 @@ import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
@@ -45,28 +40,28 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.datepicker.CalendarConstraints;
import com.google.android.material.datepicker.CompositeDateValidator;
import com.google.android.material.datepicker.DateValidatorPointBackward;
import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
@@ -101,6 +96,7 @@ import com.hippo.ehviewer.widget.SearchBar;
import com.hippo.ehviewer.widget.SearchLayout;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.scene.StageActivity;
import com.hippo.util.ExceptionUtils;
import com.hippo.view.BringOutTransition;
import com.hippo.view.ViewTransition;
@@ -109,7 +105,6 @@ import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.StringUtils;
@@ -118,6 +113,9 @@ import com.hippo.yorozuya.ViewUtils;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -145,6 +143,8 @@ public final class GalleryListScene extends BaseScene
private final static int STATE_SEARCH = 2;
private final static int STATE_SEARCH_SHOW_LIST = 3;
private static final long ANIMATE_TIME = 300L;
// Double click back exit
private final long mPressBackTime = 0;
/*---------------
Whole life cycle
---------------*/
@@ -161,8 +161,8 @@ public final class GalleryListScene extends BaseScene
private EasyRecyclerView mRecyclerView;
@Nullable
private SearchLayout mSearchLayout;
ActivityResultLauncher<String[]> selectImageLauncher = registerForActivityResult(
new ActivityResultContracts.OpenDocument(),
ActivityResultLauncher<PickVisualMediaRequest> selectImageLauncher = registerForActivityResult(
new ActivityResultContracts.PickVisualMedia(),
result -> mSearchLayout.setImageUri(result));
@Nullable
private SearchBar mSearchBar;
@@ -225,8 +225,6 @@ public final class GalleryListScene extends BaseScene
}
}
};
// Double click back exit
private long mPressBackTime = 0;
private boolean mHasFirstRefresh = false;
private int mNavCheckedId = 0;
private DownloadManager mDownloadManager;
@@ -502,6 +500,8 @@ public final class GalleryListScene extends BaseScene
mFabLayout.getSecondaryFabAt(0).setImageResource(isTopList ? R.drawable.ic_baseline_format_list_numbered_24 : R.drawable.v_magnify_x24);
}
mFabLayout.setSecondaryFabVisibilityAt(1, ListUrlBuilder.MODE_WHATS_HOT != builder.getMode());
// Update normal search mode
mSearchLayout.setNormalSearchMode(builder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION
? R.id.search_subscription_search
@@ -674,7 +674,7 @@ public final class GalleryListScene extends BaseScene
if (null == context) {
return;
}
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
final BaseDialogBuilder builder = new BaseDialogBuilder(context);
builder.setMessage(R.string.add_quick_search_tip);
builder.setTitle(R.string.readme);
builder.show();
@@ -753,19 +753,13 @@ public final class GalleryListScene extends BaseScene
Context context = getContext();
AssertUtils.assertNotNull(context);
RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager();
dragDropManager.setInitiateOnLongPress(true);
dragDropManager.setInitiateOnTouch(false);
dragDropManager.setDraggingItemAlpha(0.8f);
final EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
final QsDrawerAdapter qsDrawerAdapter = new QsDrawerAdapter(inflater);
qsDrawerAdapter.setHasStableIds(true);
final GeneralItemAnimator animator = new DraggableItemAnimator();
recyclerView.setItemAnimator(animator);
recyclerView.setAdapter(dragDropManager.createWrappedAdapter(qsDrawerAdapter));
dragDropManager.attachRecyclerView(recyclerView);
recyclerView.setAdapter(qsDrawerAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GalleryListQSItemTouchHelperCallback(qsDrawerAdapter));
itemTouchHelper.attachToRecyclerView(recyclerView);
mQuickSearchList = EhDB.getAllQuickSearch();
tip.setText(R.string.quick_search_tip);
if (mIsTopList) {
@@ -795,6 +789,21 @@ public final class GalleryListScene extends BaseScene
return view;
}
public boolean isBackpressCanPreviewLauncherStatus() {
if (null != mFabLayout && mFabLayout.isExpanded()) {
return false;
}
switch (mState) {
case STATE_NORMAL:
return getStackIndex() == 0;
case STATE_SIMPLE_SEARCH:
case STATE_SEARCH:
case STATE_SEARCH_SHOW_LIST:
return false;
}
return false;
}
@Override
public void onBackPressed() {
if (null != mFabLayout && mFabLayout.isExpanded()) {
@@ -858,36 +867,61 @@ public final class GalleryListScene extends BaseScene
return;
}
final int page = mHelper.getPageForTop();
final int pages = mHelper.getPages();
String hint = getString(R.string.go_to_hint, page + 1, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
if (ListUrlBuilder.MODE_TOPLIST == mUrlBuilder.getMode()) {
final int page = mHelper.getPageForTop() + 1;
final int pages = 200;
String hint = getString(R.string.go_to_hint, page, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goTo(goTo);
dialog.dismiss();
});
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goToPage(goTo);
dialog.dismiss();
});
} else {
LocalDateTime local = LocalDateTime.of(2007, 3, 21, 0, 0);
long fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
long toDate = MaterialDatePicker.todayInUtcMilliseconds();
ArrayList listValidators = new ArrayList<>();
listValidators.add(DateValidatorPointForward.from(fromDate));
listValidators.add(DateValidatorPointBackward.before(toDate));
var constraintsBuilder = new CalendarConstraints.Builder()
.setStart(fromDate)
.setEnd(toDate)
.setValidator(CompositeDateValidator.allOf(listValidators));
var datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(constraintsBuilder.build())
.setTitleText(R.string.go_to)
.setSelection(toDate)
.build();
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
datePicker.addOnPositiveButtonClickListener(v -> {
mHelper.goTo(v);
});
}
}
@Override
@@ -967,7 +1001,7 @@ public final class GalleryListScene extends BaseScene
favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24,
};
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(EhUtils.getSuitableTitle(gi))
.setAdapter(new SelectItemWithIconAdapter(context, items, icons), (dialog, which) -> {
switch (which) {
@@ -979,7 +1013,7 @@ public final class GalleryListScene extends BaseScene
break;
case 1: // Download
if (downloaded) {
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(getString(R.string.download_remove_dialog_message, gi.title))
.setPositiveButton(android.R.string.ok, (dialog1, which1) -> mDownloadManager.deleteDownload(gi.gid))
@@ -1006,7 +1040,7 @@ public final class GalleryListScene extends BaseScene
MoveDialogHelper helper = new MoveDialogHelper(labels, gi);
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show();
@@ -1201,6 +1235,7 @@ public final class GalleryListScene extends BaseScene
break;
}
}
((StageActivity) requireActivity()).updateBackPressCallBackStatus();
}
@Override
@@ -1352,7 +1387,9 @@ public final class GalleryListScene extends BaseScene
@Override
public void onSelectImage() {
try {
selectImageLauncher.launch(new String[]{"image/*"});
PickVisualMediaRequest.Builder builder = new PickVisualMediaRequest.Builder();
builder.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE);
selectImageLauncher.launch(builder.build());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT);
@@ -1389,7 +1426,16 @@ public final class GalleryListScene extends BaseScene
? R.string.gallery_list_empty_hit_subscription
: R.string.gallery_list_empty_hit);
mHelper.setEmptyString(emptyString);
mHelper.onGetPageData(taskId, result.pages, result.nextPage, result.galleryInfoList);
int pages = 0;
if (mIsTopList)
pages = 200;
else if (result.nextGid == 0)
pages = mHelper.pgCounter + 1;
else
pages = result.founds;
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
}
}
@@ -1492,7 +1538,7 @@ public final class GalleryListScene extends BaseScene
}
}
private static class QsDrawerHolder extends AbstractDraggableItemViewHolder {
private static class QsDrawerHolder extends RecyclerView.ViewHolder {
private final TextView key;
private final ImageView option;
@@ -1537,7 +1583,7 @@ public final class GalleryListScene extends BaseScene
}
}
private class QsDrawerAdapter extends RecyclerView.Adapter<QsDrawerHolder> implements DraggableItemAdapter<QsDrawerHolder> {
private class QsDrawerAdapter extends RecyclerView.Adapter<QsDrawerHolder> {
private final LayoutInflater mInflater;
@@ -1562,40 +1608,12 @@ public final class GalleryListScene extends BaseScene
}
mUrlBuilder.set(mQuickSearchList.get(position));
mUrlBuilder.setPageIndex(0);
mUrlBuilder.setNextGid(0);
onUpdateUrlBuilder();
mHelper.refresh();
setState(STATE_NORMAL);
closeDrawer(Gravity.RIGHT);
});
holder.itemView.setOnLongClickListener(v -> {
PopupMenu popupMenu = new PopupMenu(requireContext(), holder.option);
popupMenu.inflate(R.menu.quicksearch_option);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
final QuickSearch quickSearch = mQuickSearchList.get(position);
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_qs_remove) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_quick_search_title))
.setMessage(getString(R.string.delete_quick_search_message, quickSearch.name))
.setPositiveButton(R.string.delete, (dialog, which) -> {
EhDB.deleteQuickSearch(quickSearch);
mQuickSearchList.remove(position);
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
}
return false;
}
});
return true;
});
} else {
int[] keywords = {11, 12, 13, 15};
int[] toplists = {R.string.toplist_alltime, R.string.toplist_pastyear, R.string.toplist_pastmonth, R.string.toplist_yesterday};
@@ -1607,7 +1625,7 @@ public final class GalleryListScene extends BaseScene
}
mUrlBuilder.setKeyword(String.valueOf(keywords[position]));
mUrlBuilder.setPageIndex(0);
mUrlBuilder.setNextGid(0);
onUpdateUrlBuilder();
mHelper.refresh();
setState(STATE_NORMAL);
@@ -1631,44 +1649,6 @@ public final class GalleryListScene extends BaseScene
public int getItemCount() {
return !mIsTopList ? mQuickSearchList != null ? mQuickSearchList.size() : 0 : 4;
}
@Override
public boolean onCheckCanStartDrag(@NonNull QsDrawerHolder holder, int position, int x, int y) {
return !mIsTopList && x > holder.option.getX() && y > holder.option.getY();
}
@Override
public ItemDraggableRange onGetItemDraggableRange(@NonNull QsDrawerHolder holder, int position) {
return null;
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
if (null == mQuickSearchList) {
return;
}
EhDB.moveQuickSearch(fromPosition, toPosition);
final QuickSearch item = mQuickSearchList.remove(fromPosition);
mQuickSearchList.add(toPosition, item);
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return true;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
}
private abstract class UrlSuggestion extends SearchBar.Suggestion {
@@ -1775,15 +1755,37 @@ public final class GalleryListScene extends BaseScene
}
private class GalleryListHelper extends GalleryInfoContentHelper {
public int pgCounter = 0;
@Override
protected void getPageData(int taskId, int type, int page) {
pgCounter = page;
MainActivity activity = getMainActivity();
if (null == activity || null == mClient || null == mUrlBuilder) {
return;
}
mUrlBuilder.setPageIndex(page);
if (mIsTopList) {
mUrlBuilder.setNextGid(page);
if (jumpTo != null) {
pgCounter = Integer.parseInt(jumpTo);
mUrlBuilder.setNextGid(pgCounter);
}
} else {
int prevGid = 0, nextGid = 0;
if (page != 0) {
if (page >= mHelper.getPageForTop())
nextGid = minGid;
else
prevGid = maxGid;
}
mUrlBuilder.setPrevGid(prevGid);
mUrlBuilder.setNextGid(nextGid);
}
mUrlBuilder.setJumpTo(jumpTo);
jumpTo = null;
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) {
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_IMAGE_SEARCH);
@@ -1853,4 +1855,47 @@ public final class GalleryListScene extends BaseScene
}
}
}
}
private class GalleryListQSItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final QsDrawerAdapter mAdapter;
public GalleryListQSItemTouchHelperCallback(QsDrawerAdapter adapter) {
mAdapter = adapter;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT);
}
@SuppressLint("NotifyDataSetChanged")
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
if (fromPosition == toPosition) {
return false;
}
if (null == mQuickSearchList) {
return false;
}
EhDB.moveQuickSearch(fromPosition, toPosition);
final QuickSearch item = mQuickSearchList.remove(fromPosition);
mQuickSearchList.add(toPosition, item);
mAdapter.notifyDataSetChanged();
return true;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if (mQuickSearchList == null)
return;
int position = viewHolder.getBindingAdapterPosition();
final QuickSearch quickSearch = mQuickSearchList.get(position);
EhDB.deleteQuickSearch(quickSearch);
mQuickSearchList.remove(position);
mAdapter.notifyDataSetChanged();
}
}
}
@@ -35,8 +35,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.slider.Slider;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
@@ -45,6 +45,7 @@ import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryPreview;
import com.hippo.ehviewer.client.data.PreviewSet;
@@ -139,6 +140,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
R.layout.scene_gallery_previews, container, false);
mContentLayout.hideFastScroll();
mRecyclerView = mContentLayout.getRecyclerView();
setLiftOnScrollTargetView(mRecyclerView);
Context context = getContext();
AssertUtils.assertNotNull(context);
@@ -189,11 +191,8 @@ public class GalleryPreviewsScene extends ToolbarScene {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_previews);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
@Override
public int getMenuResId() {
return R.menu.scene_gallery_previews;
if (((GalleryDetail) mGalleryInfo).previewPages > 2)
showMenu(R.menu.scene_gallery_previews);
}
@Override
@@ -211,7 +210,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
int pages = mHelper.getPages();
if (pages > 1 && mHelper.canGoTo()) {
GoToDialogHelper helper = new GoToDialogHelper(pages, mHelper.getPageForTop());
AlertDialog dialog = new MaterialAlertDialogBuilder(context).setTitle(R.string.go_to)
AlertDialog dialog = new BaseDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
.setPositiveButton(android.R.string.ok, null)
.create();
@@ -1,697 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem;
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.HandlerDrawable;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.FavouriteStatusRouter;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhCacheKeyFactory;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.view.ViewTransition;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rikka.core.res.ResourcesKt;
public class HistoryScene extends ToolbarScene {
/*---------------
View life cycle
---------------*/
@Nullable
private TextView mTip;
@Nullable
private FastScroller mFastScroller;
@Nullable
private EasyRecyclerView mRecyclerView;
@Nullable
private ViewTransition mViewTransition;
@Nullable
private RecyclerView.Adapter<?> mAdapter;
@Nullable
private List<HistoryInfo> mLazyList;
private DownloadManager mDownloadManager;
private DownloadManager.DownloadInfoListener mDownloadInfoListener;
private FavouriteStatusRouter mFavouriteStatusRouter;
private FavouriteStatusRouter.Listener mFavouriteStatusRouterListener;
@Override
public void onDestroy() {
super.onDestroy();
mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener);
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener);
}
@Override
public int getNavCheckedItem() {
return R.id.nav_history;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
assert context != null;
mDownloadManager = EhApplication.getDownloadManager(context);
mDownloadManager = EhApplication.getDownloadManager(context);
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context);
mDownloadInfoListener = new DownloadManager.DownloadInfoListener() {
@Override
public void onAdd(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list, int position) {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onUpdate(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list) {
}
@Override
public void onUpdateAll() {
}
@Override
public void onReload() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onChange() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onRenameLabel(String from, String to) {
}
@Override
public void onRemove(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list, int position) {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onUpdateLabels() {
}
};
mDownloadManager.addDownloadInfoListener(mDownloadInfoListener);
mFavouriteStatusRouterListener = (gid, slot) -> {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
};
mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener);
}
@Nullable
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_history, container, false);
View content = ViewUtils.$$(view, R.id.content);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(content, R.id.recycler_view);
mFastScroller = (FastScroller) ViewUtils.$$(content, R.id.fast_scroller);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(content, mTip);
Context context = getContext();
AssertUtils.assertNotNull(context);
Resources resources = context.getResources();
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.big_history);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
mTip.setCompoundDrawables(null, drawable, null, null);
RecyclerViewTouchActionGuardManager guardManager = new RecyclerViewTouchActionGuardManager();
guardManager.setInterceptVerticalScrollingWhileAnimationRunning(true);
guardManager.setEnabled(true);
RecyclerViewSwipeManager swipeManager = new RecyclerViewSwipeManager();
mAdapter = new HistoryAdapter();
mAdapter.setHasStableIds(true);
mAdapter = swipeManager.createWrappedAdapter(mAdapter);
mRecyclerView.setAdapter(mAdapter);
final GeneralItemAnimator animator = new SwipeDismissItemAnimator();
animator.setSupportsChangeAnimations(false);
mRecyclerView.setItemAnimator(animator);
AutoStaggeredGridLayoutManager layoutManager = new AutoStaggeredGridLayoutManager(
0, StaggeredGridLayoutManager.VERTICAL);
layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId()));
layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setClipToPadding(false);
mRecyclerView.setClipChildren(false);
int interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval);
int paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h);
int paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v);
MarginItemDecoration decoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
mRecyclerView.addItemDecoration(decoration);
guardManager.attachRecyclerView(mRecyclerView);
swipeManager.attachRecyclerView(mRecyclerView);
mFastScroller.attachToRecyclerView(mRecyclerView);
HandlerDrawable handlerDrawable = new HandlerDrawable();
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
mFastScroller.setHandlerDrawable(handlerDrawable);
updateLazyList();
updateView(false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.history);
setNavigationIcon(R.drawable.ic_baseline_menu_24);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mLazyList) {
mLazyList = null;
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
mViewTransition = null;
mAdapter = null;
}
// Remember to notify
private void updateLazyList() {
mLazyList = EhDB.getHistoryLazyList();
}
private void updateView(boolean animation) {
if (null == mAdapter || null == mViewTransition) {
return;
}
if (mAdapter.getItemCount() == 0) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
}
@SuppressLint("RtlHardcoded")
@Override
public void onNavigationClick() {
toggleDrawer(Gravity.LEFT);
}
@Override
public int getMenuResId() {
return R.menu.scene_history;
}
private void showClearAllDialog() {
new MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.clear_all_history)
.setPositiveButton(R.string.clear_all, (dialog, which) -> {
if (DialogInterface.BUTTON_POSITIVE != which || null == mAdapter) {
return;
}
EhDB.clearHistoryInfo();
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
// Skip when in choice mode
Context context = getContext();
if (null == context) {
return false;
}
int id = item.getItemId();
if (id == R.id.action_clear_all) {
showClearAllDialog();
return true;
}
return false;
}
public boolean onItemClick(View view, int position) {
if (null == mLazyList) {
return false;
}
Bundle args = new Bundle();
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mLazyList.get(position));
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
startScene(announcer);
return true;
}
public boolean onItemLongClick(int position) {
final Context context = getContext();
final MainActivity activity = getMainActivity();
if (null == context || null == activity || null == mLazyList) {
return false;
}
final GalleryInfo gi = mLazyList.get(position);
if (gi == null) {
return true;
}
boolean downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID;
boolean favourited = gi.favoriteSlot != -2;
CharSequence[] items = downloaded ? new CharSequence[]{
context.getString(R.string.read),
context.getString(R.string.delete_downloads),
context.getString(favourited ? R.string.remove_from_favourites : R.string.add_to_favourites),
context.getString(R.string.delete),
context.getString(R.string.download_move_dialog_title),
} : new CharSequence[]{
context.getString(R.string.read),
context.getString(R.string.download),
context.getString(favourited ? R.string.remove_from_favourites : R.string.add_to_favourites),
context.getString(R.string.delete),
};
int[] icons = downloaded ? new int[]{
R.drawable.v_book_open_x24,
R.drawable.v_delete_x24,
favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24,
R.drawable.v_delete_x24,
R.drawable.v_folder_move_x24,
} : new int[]{
R.drawable.v_book_open_x24,
R.drawable.v_download_x24,
favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24,
R.drawable.v_delete_x24,
};
new MaterialAlertDialogBuilder(context)
.setTitle(EhUtils.getSuitableTitle(gi))
.setAdapter(new SelectItemWithIconAdapter(context, items, icons), (dialog, which) -> {
switch (which) {
case 0: // Read
Intent intent = new Intent(activity, GalleryActivity.class);
intent.setAction(GalleryActivity.ACTION_EH);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi);
startActivity(intent);
break;
case 1: // Download
if (downloaded) {
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(getString(R.string.download_remove_dialog_message, gi.title))
.setPositiveButton(android.R.string.ok, (dialog1, which1) -> mDownloadManager.deleteDownload(gi.gid))
.show();
} else {
CommonOperations.startDownload(activity, gi, false);
}
break;
case 2: // Favorites
if (favourited) {
CommonOperations.removeFromFavorites(activity, gi, new RemoveFromFavoriteListener(context, activity.getStageId(), getTag()));
} else {
CommonOperations.addToFavorites(activity, gi, new AddToFavoriteListener(context, activity.getStageId(), getTag()));
}
break;
case 3: // Delete
if (null == mLazyList || null == mAdapter) {
return;
}
HistoryInfo info = mLazyList.get(position);
EhDB.deleteHistoryInfo(info);
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
break;
case 4: // Move
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager(context).getLabelList();
List<String> labelList = new ArrayList<>(labelRawList.size() + 1);
labelList.add(getString(R.string.default_download_label_name));
for (int i = 0, n = labelRawList.size(); i < n; i++) {
labelList.add(labelRawList.get(i).getLabel());
}
String[] labels = labelList.toArray(new String[0]);
MoveDialogHelper helper = new MoveDialogHelper(labels, gi);
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show();
break;
}
}).show();
return true;
}
private static class AddToFavoriteListener extends EhCallback<GalleryListScene, Void> {
public AddToFavoriteListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(Void result) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT);
}
@Override
public void onFailure(Exception e) {
showTip(R.string.add_to_favorite_failure, LENGTH_LONG);
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof GalleryListScene;
}
}
private static class RemoveFromFavoriteListener extends EhCallback<GalleryListScene, Void> {
public RemoveFromFavoriteListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(Void result) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT);
}
@Override
public void onFailure(Exception e) {
showTip(R.string.remove_from_favorite_failure, LENGTH_LONG);
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof GalleryListScene;
}
}
private static class HistoryHolder extends AbstractSwipeableItemViewHolder {
public final View card;
public final LoadImageView thumb;
public final TextView title;
public final TextView uploader;
public final SimpleRatingView rating;
public final TextView category;
public final TextView posted;
public final TextView simpleLanguage;
public final TextView pages;
public final ImageView downloaded;
public HistoryHolder(View itemView) {
super(itemView);
card = itemView.findViewById(R.id.card);
thumb = itemView.findViewById(R.id.thumb);
title = itemView.findViewById(R.id.title);
uploader = itemView.findViewById(R.id.uploader);
rating = itemView.findViewById(R.id.rating);
category = itemView.findViewById(R.id.category);
posted = itemView.findViewById(R.id.posted);
simpleLanguage = itemView.findViewById(R.id.simple_language);
pages = itemView.findViewById(R.id.pages);
downloaded = itemView.findViewById(R.id.downloaded);
}
@NonNull
@Override
public View getSwipeableContainerView() {
return card;
}
}
private class MoveDialogHelper implements DialogInterface.OnClickListener {
private final String[] mLabels;
private final GalleryInfo mGi;
public MoveDialogHelper(String[] labels, GalleryInfo gi) {
mLabels = labels;
mGi = gi;
}
@Override
public void onClick(DialogInterface dialog, int which) {
// Cancel check mode
Context context = getContext();
if (null == context) {
return;
}
if (null != mRecyclerView) {
mRecyclerView.outOfCustomChoiceMode();
}
DownloadManager downloadManager = EhApplication.getDownloadManager(context);
DownloadInfo downloadInfo = downloadManager.getDownloadInfo(mGi.gid);
if (downloadInfo == null) {
return;
}
String label = which == 0 ? null : mLabels[which];
downloadManager.changeLabel(Collections.singletonList(downloadInfo), label);
}
}
private class HistoryAdapter extends RecyclerView.Adapter<HistoryHolder>
implements SwipeableItemAdapter<HistoryHolder> {
private final LayoutInflater mInflater;
private final int mListThumbWidth;
private final int mListThumbHeight;
public HistoryAdapter() {
mInflater = getLayoutInflater();
@SuppressLint("InflateParams") View calculator = mInflater.inflate(R.layout.item_gallery_list_thumb_height, null);
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT);
mListThumbHeight = calculator.getMeasuredHeight();
mListThumbWidth = mListThumbHeight * 2 / 3;
}
@Override
public long getItemId(int position) {
if (null == mLazyList) {
return super.getItemId(position);
} else {
return mLazyList.get(position).gid;
}
}
@NonNull
@Override
public HistoryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
HistoryHolder holder = new HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false));
ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams();
lp.width = mListThumbWidth;
lp.height = mListThumbHeight;
holder.thumb.setLayoutParams(lp);
return holder;
}
@Override
public void onBindViewHolder(@NonNull HistoryHolder holder, int position) {
if (null == mLazyList) {
return;
}
GalleryInfo gi = mLazyList.get(position);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
holder.title.setText(EhUtils.getSuitableTitle(gi));
holder.uploader.setText(gi.uploader);
holder.rating.setRating(gi.rating);
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.category);
if (!newCategoryText.contentEquals(category.getText())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category));
}
holder.posted.setText(gi.posted);
holder.pages.setText(null);
holder.pages.setVisibility(View.GONE);
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.setText(null);
holder.simpleLanguage.setVisibility(View.GONE);
} else {
holder.simpleLanguage.setText(gi.simpleLanguage);
holder.simpleLanguage.setVisibility(View.VISIBLE);
}
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.gid) ? View.VISIBLE : View.GONE);
// Update transition name
long gid = gi.gid;
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gid));
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, position));
holder.card.setOnLongClickListener(v -> onItemLongClick(position));
}
@Override
public int getItemCount() {
return null != mLazyList ? mLazyList.size() : 0;
}
@Override
public int onGetSwipeReactionType(@NonNull HistoryHolder holder, int position, int x, int y) {
return SwipeableItemConstants.REACTION_CAN_SWIPE_LEFT;
}
@Override
public void onSwipeItemStarted(@NonNull HistoryHolder holder, int position) {
}
@Override
public void onSetSwipeBackground(@NonNull HistoryHolder holder, int position, int type) {
}
@Override
public SwipeResultAction onSwipeItem(@NonNull HistoryHolder holder, int position, int result) {
switch (result) {
case SwipeableItemConstants.RESULT_SWIPED_LEFT:
return new SwipeResultActionClear(position);
case SwipeableItemConstants.RESULT_SWIPED_RIGHT:
case SwipeableItemConstants.RESULT_CANCELED:
case SwipeableItemConstants.RESULT_NONE:
case SwipeableItemConstants.RESULT_SWIPED_DOWN:
case SwipeableItemConstants.RESULT_SWIPED_UP:
default:
return new SwipeResultActionDefault();
}
}
}
private class SwipeResultActionClear extends SwipeResultActionRemoveItem {
private final int mPosition;
protected SwipeResultActionClear(int position) {
mPosition = position;
}
@Override
protected void onPerformAction() {
super.onPerformAction();
if (null == mLazyList || null == mAdapter) {
return;
}
HistoryInfo info = mLazyList.get(mPosition);
EhDB.deleteHistoryInfo(info);
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
}
}
}
@@ -0,0 +1,459 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingDataAdapter
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.FastScroller
import com.hippo.easyrecyclerview.HandlerDrawable
import com.hippo.ehviewer.*
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.GalleryActivity
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.scene.Announcer
import com.hippo.scene.SceneFragment
import com.hippo.view.ViewTransition
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.ViewUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
class HistoryScene : ToolbarScene() {
private var mRecyclerView: RecyclerView? = null
private val mAdapter: HistoryAdapter by lazy {
HistoryAdapter(object : DiffUtil.ItemCallback<HistoryInfo>() {
override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
})
}
private val mDownloadManager: DownloadManager by lazy {
EhApplication.getDownloadManager(requireContext())
}
private val mDownloadInfoListener: DownloadInfoListener by lazy {
object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter.notifyDataSetChanged()
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}
override fun onUpdateAll() {}
override fun onReload() {
mAdapter.notifyDataSetChanged()
}
override fun onChange() {
mAdapter.notifyDataSetChanged()
}
override fun onRenameLabel(from: String, to: String) {}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter.notifyDataSetChanged()
}
override fun onUpdateLabels() {}
}
}
private val mFavouriteStatusRouter: FavouriteStatusRouter by lazy {
EhApplication.getFavouriteStatusRouter(requireContext())
}
private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener by lazy {
FavouriteStatusRouter.Listener { _: Long, _: Int ->
mAdapter.notifyDataSetChanged()
}
}
override fun onDestroy() {
super.onDestroy()
mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener)
}
override fun getNavCheckedItem(): Int {
return R.id.nav_history
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDownloadManager.addDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.scene_history, container, false)
val content = ViewUtils.`$$`(view, R.id.content)
val recyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as RecyclerView
setLiftOnScrollTargetView(recyclerView)
val mFastScroller = ViewUtils.`$$`(content, R.id.fast_scroller) as FastScroller
val mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
ViewTransition(content, mTip)
val resources = requireContext().resources
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_history)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip.setCompoundDrawables(null, drawable, null, null)
val historyData = Pager(
PagingConfig(20)
) {
EhDB.getHistoryLazyList()
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
recyclerView.adapter = mAdapter
val layoutManager = AutoStaggeredGridLayoutManager(
0, StaggeredGridLayoutManager.VERTICAL
)
layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId()))
layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
val itemTouchHelper = ItemTouchHelper(HistoryItemTouchHelperCallback())
itemTouchHelper.attachToRecyclerView(recyclerView)
mFastScroller.attachToRecyclerView(recyclerView)
mRecyclerView = recyclerView
val handlerDrawable = HandlerDrawable()
handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary))
mFastScroller.setHandlerDrawable(handlerDrawable)
viewLifecycleOwner.lifecycleScope.launch {
historyData.collectLatest { value ->
mAdapter.submitData(
value
)
}
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.history)
setNavigationIcon(R.drawable.ic_baseline_menu_24)
}
override fun onDestroyView() {
super.onDestroyView()
mRecyclerView?.stopScroll()
mRecyclerView = null
}
@SuppressLint("RtlHardcoded")
override fun onNavigationClick() {
toggleDrawer(Gravity.LEFT)
}
override fun getMenuResId(): Int {
return R.menu.scene_history
}
@SuppressLint("NotifyDataSetChanged")
private fun showClearAllDialog() {
BaseDialogBuilder(requireContext())
.setMessage(R.string.clear_all_history)
.setPositiveButton(R.string.clear_all) { _: DialogInterface?, which: Int ->
if (DialogInterface.BUTTON_POSITIVE != which) {
return@setPositiveButton
}
EhDB.clearHistoryInfo()
mAdapter.refresh()
mAdapter.notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.action_clear_all) {
showClearAllDialog()
return true
}
return false
}
fun onItemClick(gi: GalleryInfo): Boolean {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)
val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)
startScene(announcer)
return true
}
fun onItemLongClick(gi: GalleryInfo): Boolean {
val context = requireContext()
val activity = mainActivity ?: return false
val downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID
val favourited = gi.favoriteSlot != -2
val items = if (downloaded) arrayOf<CharSequence>(
context.getString(R.string.read),
context.getString(R.string.delete_downloads),
context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),
context.getString(R.string.download_move_dialog_title)
) else arrayOf<CharSequence>(
context.getString(R.string.read),
context.getString(R.string.download),
context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),
)
val icons = if (downloaded) intArrayOf(
R.drawable.v_book_open_x24,
R.drawable.v_delete_x24,
if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,
R.drawable.v_folder_move_x24
) else intArrayOf(
R.drawable.v_book_open_x24,
R.drawable.v_download_x24,
if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,
)
BaseDialogBuilder(context)
.setTitle(EhUtils.getSuitableTitle(gi))
.setAdapter(
SelectItemWithIconAdapter(
context,
items,
icons
)
) { _: DialogInterface?, which: Int ->
when (which) {
0 -> {
val intent = Intent(activity, GalleryActivity::class.java)
intent.action = GalleryActivity.ACTION_EH
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi)
startActivity(intent)
}
1 -> if (downloaded) {
BaseDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(
getString(
R.string.download_remove_dialog_message,
gi.title
)
)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
mDownloadManager.deleteDownload(
gi.gid
)
}
.show()
} else {
CommonOperations.startDownload(activity, gi, false)
}
2 -> if (favourited) {
CommonOperations.removeFromFavorites(
activity,
gi,
RemoveFromFavoriteListener(context, activity.stageId, tag)
)
} else {
CommonOperations.addToFavorites(
activity,
gi,
AddToFavoriteListener(context, activity.stageId, tag)
)
}
3 -> {
val labelRawList = EhApplication.getDownloadManager(context).labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
labelList.add(getString(R.string.default_download_label_name))
var i = 0
val n = labelRawList.size
while (i < n) {
labelList.add(labelRawList[i].label)
i++
}
val labels = labelList.toTypedArray()
val helper = MoveDialogHelper(labels, gi)
BaseDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show()
}
}
}.show()
return true
}
private class AddToFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
override fun onSuccess(result: Void?) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT)
}
override fun onFailure(e: Exception) {
showTip(R.string.add_to_favorite_failure, LENGTH_LONG)
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class RemoveFromFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
override fun onSuccess(result: Void?) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)
}
override fun onFailure(e: Exception) {
showTip(R.string.remove_from_favorite_failure, LENGTH_LONG)
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class HistoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val card: View = itemView.findViewById(R.id.card)
val thumb: LoadImageView = itemView.findViewById(R.id.thumb)
val title: TextView = itemView.findViewById(R.id.title)
val uploader: TextView = itemView.findViewById(R.id.uploader)
val rating: SimpleRatingView = itemView.findViewById(R.id.rating)
val category: TextView = itemView.findViewById(R.id.category)
val posted: TextView = itemView.findViewById(R.id.posted)
val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)
val pages: TextView = itemView.findViewById(R.id.pages)
val downloaded: ImageView = itemView.findViewById(R.id.downloaded)
}
private inner class MoveDialogHelper(
private val mLabels: Array<String>,
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val downloadManager = EhApplication.getDownloadManager(requireContext())
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
downloadManager.changeLabel(listOf(downloadInfo), label)
}
}
private inner class HistoryAdapter(diffCallback: DiffUtil.ItemCallback<HistoryInfo>) :
PagingDataAdapter<HistoryInfo, HistoryHolder>(diffCallback) {
private val mInflater: LayoutInflater = layoutInflater
private val mListThumbWidth: Int
private val mListThumbHeight: Int
init {
@SuppressLint("InflateParams") val calculator =
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
mListThumbHeight = calculator.measuredHeight
mListThumbWidth = mListThumbHeight * 2 / 3
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryHolder {
val holder = HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false))
val lp = holder.thumb.layoutParams
lp.width = mListThumbWidth
lp.height = mListThumbHeight
holder.thumb.layoutParams = lp
return holder
}
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
val gi: GalleryInfo? = getItem(position)
gi ?: return
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb)
holder.title.text = EhUtils.getSuitableTitle(gi)
holder.uploader.text = gi.uploader
holder.rating.rating = gi.rating
val category = holder.category
val newCategoryText = EhUtils.getCategory(gi.category)
if (!newCategoryText.contentEquals(category.text)) {
category.text = newCategoryText
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))
}
holder.posted.text = gi.posted
holder.pages.text = null
holder.pages.visibility = View.GONE
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.text = null
holder.simpleLanguage.visibility = View.GONE
} else {
holder.simpleLanguage.text = gi.simpleLanguage
holder.simpleLanguage.visibility = View.VISIBLE
}
holder.downloaded.visibility =
if (mDownloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
holder.card.setOnClickListener { onItemClick(gi) }
holder.card.setOnLongClickListener { onItemLongClick(gi) }
}
}
private inner class HistoryItemTouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val mPosition = viewHolder.bindingAdapterPosition
val info: HistoryInfo? = mAdapter.peek(mPosition)
info?.let { EhDB.deleteHistoryInfo(info) }
mAdapter.refresh()
}
}
}
@@ -30,8 +30,8 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
@@ -333,7 +333,7 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
return;
}
new MaterialAlertDialogBuilder(context)
new BaseDialogBuilder(context)
.setTitle(R.string.sign_in_failed)
.setMessage(ExceptionUtils.getReadableString(e) + "\n\n" + getString(R.string.sign_in_failed_tip))
.setPositiveButton(R.string.get_it, null)
@@ -20,7 +20,6 @@ import android.os.Bundle;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.scene.Announcer;
/**
@@ -1,130 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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 com.hippo.ehviewer.ui.scene;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import com.hippo.ehviewer.R;
public abstract class ToolbarScene extends BaseScene {
@Nullable
private Toolbar mToolbar;
private CharSequence mTempTitle;
@Nullable
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return null;
}
@Nullable
@Override
public final View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_toolbar, container, false);
Toolbar toolbar = view.findViewById(R.id.toolbar);
FrameLayout contentPanel = view.findViewById(R.id.content_panel);
View contentView = onCreateViewWithToolbar(inflater, contentPanel, savedInstanceState);
if (contentView == null) {
return null;
} else {
mToolbar = toolbar;
contentPanel.addView(contentView, 0);
return view;
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mToolbar = null;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mToolbar != null) {
if (mTempTitle != null) {
mToolbar.setTitle(mTempTitle);
mTempTitle = null;
}
int menuResId = getMenuResId();
if (menuResId != 0) {
mToolbar.inflateMenu(menuResId);
mToolbar.setOnMenuItemClickListener(ToolbarScene.this::onMenuItemClick);
}
mToolbar.setNavigationOnClickListener(v -> onNavigationClick());
}
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
public int getMenuResId() {
return 0;
}
public boolean onMenuItemClick(MenuItem item) {
return false;
}
public void onNavigationClick() {
}
public void setNavigationIcon(@DrawableRes int resId) {
if (mToolbar != null) {
mToolbar.setNavigationIcon(resId);
}
}
public void setNavigationIcon(@Nullable Drawable icon) {
if (mToolbar != null) {
mToolbar.setNavigationIcon(icon);
}
}
public void setTitle(@StringRes int resId) {
setTitle(getString(resId));
}
public void setTitle(CharSequence title) {
if (mToolbar != null) {
mToolbar.setTitle(title);
} else {
mTempTitle = title;
}
}
}
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more