Compare commits

..
Author SHA1 Message Date
txyyh 6f3476895e Rel 1.7.31.0-alpha02
Releases / Build (push) Has been cancelled
2022-11-03 15:56:49 +00:00
tarsin 98af874e83 Don't parse found in common path 2022-11-03 15:12:55 +00:00
tarsin 02bd5badb9 Fix tag search page load 2022-11-03 15:02:54 +00:00
tarsin 605f86483b Fix favourite scene data restore 2022-11-03 15:00:45 +00:00
tarsin f0d6ad9848 Fix uploader page request 2022-11-03 14:51:35 +00:00
tarsin 7677498e0c Relax founds limit
We will calculate accurately later
2022-11-03 14:51:14 +00:00
tarsin d115af60b3 Fix restore state 2022-11-03 14:50:55 +00:00
tarsin b07ab230e9 Code clean and allow refresh pages 2022-11-03 14:50:32 +00:00
tarsin 760ca8d6fb Minimal changes to rework Favourite grabs
TODO: Reimpl goto, refresh pages
And take care of per page counts by user
2022-11-03 05:39:36 +00:00
tarsin 4f0acb465a Minimal changes to rework new exhentai page load
TODO: Reimpl Favourite grab, goto, refresh pages
2022-11-03 05:39:22 +00:00
renovate[bot] f1ddc6c3aa Update lifecycle_version to v2.6.0-alpha03 (1.7.x.x) (#346)
Update lifecycle_version to v2.6.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 17:28:50 +00:00
renovate[bot] 3b955cc9a9 Update fragment_version to v1.5.4 (1.7.x.x) (#345)
Update fragment_version to v1.5.4

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 14:30:30 +00:00
renovate[bot] 652f503635 Update dependency com.google.android.material:material to v1.8.0-alpha02 (1.7.x.x) (#344)
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 11:05:23 +00:00
renovate[bot] 5f6d2e8dbd Update activity_version to v1.6.1 (1.7.x.x) (#343)
Update activity_version to v1.6.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 07:22:55 +00:00
renovate[bot] 4925312c4a Update plugin com.android.application to v7.3.1 (1.7.x.x) (#328)
Update plugin com.android.application to v7.3.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-13 20:41:56 +00:00
renovate[bot] c58bad59e2 Update plugin org.jetbrains.kotlin.android to v1.7.20 (1.7.x.x) (#315)
Update plugin org.jetbrains.kotlin.android to v1.7.20

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-29 14:52:04 +00:00
renovate[bot] 7aa318890d Update dependency androidx.annotation:annotation to v1.5.0 (1.7.x.x) (#306)
Update dependency androidx.annotation:annotation to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 19:42:22 +08:00
renovate[bot] 066500f2f8 Update fragment_version to v1.5.3 (1.7.x.x) (#304)
Update fragment_version to v1.5.3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 13:34:46 +08:00
renovate[bot] d37752a6db Update dependency androidx.recyclerview:recyclerview to v1.3.0-rc01 (1.7.x.x) (#303)
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 13:34:32 +08:00
renovate[bot] cd81a10d28 Update dependency androidx.customview:customview to v1.2.0-alpha02 (1.7.x.x) (#301)
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 10:44:48 +08:00
renovate[bot] 812018af06 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha04 (1.7.x.x) (#300)
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 10:44:36 +08:00
renovate[bot] ee1ddeec12 Update activity_version to v1.6.0 (1.7.x.x) (#298)
Update activity_version to v1.6.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 10:30:44 +08:00
renovate[bot] 8a3ce0e5ef Update dependency androidx.biometric:biometric-ktx to v1.2.0-alpha05 (1.7.x.x) (#299)
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 10:30:34 +08:00
LaLucid 8b9a3a0274 Add Gradle cache clean (#287) 2022-09-18 10:05:57 +00:00
txyyh 08f8f6cc75 Use example version code in issue template 2022-09-17 08:53:21 +00:00
txyyh 1052ac5fc5 Apply format to all docs 2022-09-16 12:43:49 +00:00
tarsin b62b6f95a8 Update README.md 2022-09-16 12:43:38 +00:00
renovate[bot] ffd719a104 Update plugin com.android.application to v7.3.0 (1.7.x.x) (#280)
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:41 +00:00
txyyh 2f787ca94b Add thanks to issue template (#272)
* Add thanks

* Shorten description
2022-09-12 14:44:09 +00:00
renovate[bot] 990c8ed9c9 Update dependency androidx.annotation:annotation-experimental to v1.3.0 (1.7.x.x) (#270)
Update dependency androidx.annotation:annotation-experimental to v1.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-09 08:51:39 +08:00
renovate[bot] 3492593a06 Update lifecycle_version to v2.6.0-alpha02 (1.7.x.x) (#269)
Update lifecycle_version to v2.6.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-09 08:51:23 +08:00
renovate[bot] 2bea46b15b Update core_version to v1.9.0 (1.7.x.x) (#267)
Update core_version to v1.9.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-09 07:09:08 +08:00
renovate[bot] f0903fa8b2 Update dependency androidx.appcompat:appcompat to v1.6.0-rc01 (1.7.x.x) (#268)
Update dependency androidx.appcompat:appcompat to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-09 07:08:39 +08:00
renovate[bot] 60caa099e2 Update activity_version to v1.6.0-rc02 (1.7.x.x) (#266)
Update activity_version to v1.6.0-rc02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 20:16:00 +00:00
txyyh 586fba4221 Add additional context textarea (#255)
* Use other word

* Add additional context to bug report
2022-09-07 16:20:06 +08:00
EdwardWu 80faa9a2db 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 05:48:24 +00:00
LittleLoli 54fea2929f add backup remote favorite list to local. (#252) 2022-09-06 00:47:02 +00:00
EdwardWu 4599ed88ee 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 16:00:26 +00:00
txyyh 60d75fd780 Update material-components (#249) 2022-09-03 11:14:53 +00:00
tarsin 896d71b867 Fix #246 2022-08-31 08:50:07 +08:00
renovate[bot] 6d440730fd Update dependency com.google.android.material:material to v1.7.0-rc01 (1.7.x.x) (#248)
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:27 +08:00
txyyh 77a6960e90 Use label on info card (#243) 2022-08-29 08:26:52 +00:00
txyyh 215eb267fe Use LabelLarge on CardMessage (#244) 2022-08-29 08:26:38 +00:00
txyyh 2c86e8b898 Use title small on comment (#242) 2022-08-29 08:26:23 +00:00
tarsin d7daa47c73 Make download card takes proper round corner 2022-08-26 07:00:27 +00:00
tarsin 3c488916bf Drop unused code 2022-08-26 07:00:27 +00:00
tarsin ba09fcdc3c Drop unused color res
* Color should be dynamic most time
2022-08-26 07:00:27 +00:00
tarsin 385101a729 Make swiperefreshlayout and ProgressIndicator follow dyncolor 2022-08-26 07:00:27 +00:00
renovate[bot] 002c3cf1d5 Update dependency androidx.webkit:webkit to v1.5.0 (1.7.x.x) (#239)
Update dependency androidx.webkit:webkit to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 19:27:40 +00:00
renovate[bot] 84064549ac Update core_version to v1.9.0-rc01 (1.7.x.x) (#238)
Update core_version to v1.9.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 15:46:28 +00:00
renovate[bot] 8c52ad212e Update activity_version to v1.6.0-rc01 (1.7.x.x) (#237)
Update activity_version to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 14:40:24 +08:00
renovate[bot] 23e6f78067 Update dependency org.jsoup:jsoup to v1.15.3 (1.7.x.x) (#226)
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:58 +00:00
tarsin d5fcb06e8f Allow universal apk build
Releases / Build (push) Has been cancelled
2022-08-24 02:52:04 +00:00
txyyh 562d1f9a0d Make release 2022-08-24 01:51:21 +00:00
tarsin 03f29288ad Fix a wrong place res 2022-08-23 07:35:18 +00:00
tarsin eb84320225 Support resize thumb, Implement #193 2022-08-23 07:34:57 +00:00
tarsin 10d73ab639 IsLightStatusBar always follow night mode, Fix #199 2022-08-23 07:34:28 +00:00
tarsin cac8f6361a Rename .java to .kt 2022-08-23 07:34:10 +00:00
tarsin 32d0758fdc Fix #196 2022-08-23 02:12:18 +00:00
tarsin c382c9c896 Add back doh 2022-08-23 02:12:18 +00:00
tarsin e772a52bf2 Fix thumb card margin 2022-08-22 10:00:38 +00:00
tarsin edda52159e Make default label removable 2022-08-22 09:58:27 +00:00
tarsin 00c2e683bd Make downloadList qs drawer drag to rearrange position and swipe to remove 2022-08-22 09:58:27 +00:00
tarsin a88766db56 Make gallerylist qs drawer drag to rearrange position and swipe to remove 2022-08-22 09:58:27 +00:00
tarsin f6c1092cba Reimplement history swipe-to-remove 2022-08-22 09:58:27 +00:00
tarsin a15b92333a Clean up readme 2022-08-22 09:58:27 +00:00
tarsin 21fc20a0f4 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-22 09:58:27 +00:00
txyyh bf72d772b5 Describe CI pages 2022-08-15 15:34:49 +00:00
txyyh 76d8575398 Rename apks on releases 2022-08-15 15:34:02 +00:00
renovate[bot] 029dc7ac61 Update dependency com.google.android.material:material to v1.7.0-beta01 2022-08-11 22:02:38 +00:00
renovate[bot] 0b6f8d1d6f Update fragment_version to v1.5.2 (1.7.x.x) (#189)
Update fragment_version to v1.5.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 13:33:21 +08:00
renovate[bot] 0b283bb333 Update dependency androidx.recyclerview:recyclerview to v1.3.0-beta02 (1.7.x.x) (#187)
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 13:21:34 +08:00
renovate[bot] c74ee2b5cb Update dependency androidx.webkit:webkit to v1.5.0-rc01 (1.7.x.x) (#188)
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 13:21:19 +08:00
renovate[bot] 8263dce3ca Update core_version to v1.9.0-beta01 (1.7.x.x) (#185)
Update core_version to v1.9.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 12:53:03 +08:00
renovate[bot] a37e5e1ab3 Update dependency androidx.appcompat:appcompat to v1.6.0-beta01 (1.7.x.x) (#186)
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 12:52:47 +08:00
txyyh 0baad6c042 Disable blank issue 2022-08-11 01:33:03 +00:00
renovate[bot] a2da9ffea6 Update activity_version to v1.6.0-beta01 (1.7.x.x) (#184)
Update activity_version to v1.6.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:10:51 +08:00
txyyh 4f3668dae4 Fix submodule error 2022-08-10 12:57:58 +00:00
txyyh 2598b68b97 Add LICENSE header 2022-08-10 12:44:56 +00:00
txyyh 7d065fb5c5 Use ic_launcher_foreground on scene_login (#174) 2022-08-10 17:10:47 +08:00
txyyh c4cc089eca Use new icon (#173)
* Use new icon

* Add monochrome icon

* Add LICENSE header
2022-08-10 17:10:47 +08:00
txyyh 85c377713d Only build CI on branches (#171) 2022-08-08 01:05:14 +08:00
renovate[bot] 3e7a1d9d35 Update dependency gradle to v7.5.1 (1.7.x.x) (#168)
Update dependency gradle to v7.5.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-06 07:42:36 +08:00
txyyh b0e9d8ec66 Fix wrong thanks 2022-08-05 18:33:18 +08:00
txyyh c25163b3c4 Revert "Use O3 optimization"
This reverts commit d396cae207.
2022-08-05 18:31:19 +08:00
txyyh 12d5aa9e08 Fix wrong TOC 2022-08-05 16:33:34 +08:00
txyyh 9fe1743dd9 Fix wrong Download section 2022-08-05 16:25:55 +08:00
txyyh e178880eb4 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 15:48:17 +08:00
txyyh 0abab346ab Delete useless string (#161) 2022-08-05 15:25:16 +08:00
EdwardWuandtarsin 63d355ad95 fixup! Ehviewer: Add Chinese(Traditional) Translate (#160)
* Ehviewer: Add Chinese(Traditional) Translate

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

* fixup! Ehviewer: Add Chinese(Traditional) Translate

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

Co-authored-by: tarsin <43900799+asuka-mio@users.noreply.github.com>
2022-08-05 15:25:03 +08:00
renovate[bot] 17ab6c7532 Update plugin com.android.application to v7.2.2 (1.7.x.x) (#163)
Update plugin com.android.application to v7.2.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-04 04:59:20 +08:00
txyyhandEdwardWu 8d82fc39f6 Ehviewer: Add Chinese(Traditional) Translate (#158)
Signed-off-by: Edward Wu <bluehome.wu@gmail.com>

Co-authored-by: EdwardWu <56945729+bluehomewu@users.noreply.github.com>
2022-08-03 21:30:14 +08:00
tarsin d396cae207 Use O3 optimization 2022-08-02 14:21:41 +08:00
tarsin ec96ca2d7c Update README.md 2022-08-02 14:13:29 +08:00
LaLucid fa2279e480 update CI (#155)
* update CI

* do not need setup-android
2022-08-02 14:13:20 +08:00
txyyh de5b77cc96 Rewrite download section in README (#151)
* Delete CI Download in README

* Add Chinese NOTE

* Add English NOTE

* Use icon for github download
2022-07-31 12:34:41 +08:00
txyyh 41a54b76bf Release 1.7.29.3
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-31 00:16:18 +08:00
txyyh a09940eb86 Turn app link dialog to acquire update (#147) 2022-07-30 23:49:57 +08:00
txyyh 159b8c13b0 Fix archive open error (#145)
This reverts commit d2aecb5eb5 and use https://github.com/Ehviewer-Overhauled/Ehviewer/commit/d2aecb5eb5e25fd5a50726485fd53de178d45aa9#r79616069
2022-07-28 14:54:49 +08:00
renovate[bot] 498d51ec22 Update fragment_version to v1.5.1 (#141)
Update fragment_version to v1.5.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-28 04:25:44 +08:00
renovate[bot] 24e50da7c9 Update collection_version to v1.3.0-alpha02 (#140)
Update collection_version to v1.3.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-28 04:25:05 +08:00
tarsin fcc595d9a3 Drop taptargetview
* Not MD3
2022-07-25 23:13:27 +08:00
tarsin d2aecb5eb5 Log tweaks 2022-07-25 23:09:50 +08:00
txyyh e2cfa30052 Release 1.7.29.2
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-25 14:54:18 +08:00
txyyh 513886eff4 pick gallery_detail_header.xml changes to television (#131)
* Rewrite info TableLayout to ConstraintLayout for television

* Rewrite layout and minor design improve for television
2022-07-25 07:50:58 +08:00
txyyh 0093db9986 Make buttongroup horizontal (#125)
* Use another impl to create space

* Horizontal button
2022-07-25 07:50:41 +08:00
txyyh 182e3a0d40 Rewrite README description (#124) 2022-07-24 17:40:19 +08:00
txyyh 6d140aaa9d Delete FOSSA (#123) 2022-07-24 14:24:31 +08:00
tarsin 17248d3a82 Revert "Use SMAPSHOT version of mdc"
This reverts commit 7a0fab29bc.
2022-07-24 13:07:28 +08:00
txyyh ec80e7a189 Back to corner (#121) 2022-07-24 05:36:07 +08:00
txyyh 493eeadf1b Add note for CI download (#122) 2022-07-24 03:35:28 +08:00
tarsin 7a0fab29bc Use SMAPSHOT version of mdc 2022-07-23 20:41:02 +08:00
hathlife 5f7e40f258 Gallery detail head tv layout (#96)
* Make height of App logo ImageView match parent

* Add TV layout for gallery detail scene.

* Add credits

* Use GPLv3 for all TV layout code
2022-07-23 20:27:42 +08:00
txyyh 0bf6b8d827 Release 1.7.29.1
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-23 17:14:25 +08:00
tarsin 4c6ccab051 Native lib info refactor 2022-07-23 16:25:53 +08:00
tarsin 00377f15b9 Set our own libs 2022-07-23 16:07:23 +08:00
tarsin 96b50eb5aa Add nettle for encrypt 2022-07-23 15:51:43 +08:00
tarsin acdf681d40 Add gnutls libnettle 2022-07-23 14:26:17 +08:00
tarsin b92cf9746b Add .gz .xz .tar .cbz .cbr to supported mimeTypes 2022-07-23 13:28:41 +08:00
txyyh d0dfbaaae0 Rewrite layout and minor design improve (#116)
* Use ConstraintLayout instead

* Specify info Card Text to BodySmall

* Specify thumb shape to none (no corner)
2022-07-23 07:44:33 +08:00
txyyh 0c656e617b Add CI download link (#117)
* Add CI Download link

* Use head in right way
2022-07-23 00:49:03 +08:00
tarsin 148b1a6344 Release 1.7.29.0
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-22 23:12:27 +08:00
tarsin 1fe0b079cd Support auto store archive's passwd 2022-07-22 22:53:44 +08:00
tarsin 87505aa989 Code clean 2022-07-22 22:18:41 +08:00
tarsin 3d7a71647e Initial support for encrypted archive read 2022-07-22 21:28:19 +08:00
txyyh c9d2f698b6 Add MDC-Android to Thanks (#113) 2022-07-22 16:51:52 +08:00
tarsin 233adbddae Drop another spinner 2022-07-22 14:14:29 +08:00
tarsin 6900d8a8c9 Rename .java to .kt 2022-07-22 14:14:29 +08:00
tarsin 640f75d8d9 Finally our data is no need to pass any jByteArray 2022-07-22 10:08:04 +08:00
tarsin 4381eb8c3d Fuck off the buffer 2022-07-22 09:52:35 +08:00
tarsin 3026b16d70 Don't emulate posix read, do mmap instead 2022-07-22 00:13:53 +08:00
txyyh ac3662a469 Release 1.7.28.4
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-22 00:07:31 +08:00
tarsin 912e6c5a5b Fix a miscompare 2022-07-21 22:25:50 +08:00
txyyh 6a73a51f63 Rewrite info TableLayout to ConstraintLayout (#111) 2022-07-21 22:13:07 +08:00
tarsin 768b6bdda7 Don't rewind fd if we can continue read 2022-07-21 22:11:50 +08:00
tarsin 9ea7cd5f7c JNIEnv is shared in the same thread 2022-07-21 21:56:32 +08:00
tarsin a91547ace7 Use in-place decompress 2022-07-21 20:47:55 +08:00
tarsin ed58085b8f Update AndroidManifest.xml
https://developer.android.google.cn/about/versions/13/behavior-changes-13?hl=zh-cn#migration
2022-07-20 21:10:07 +08:00
txyyh 944e267795 Separate different action (#107)
* Rename ci action

* Separate push and pull request action

* Change README CI shield

* Rename artifact to right name
2022-07-20 19:22:59 +08:00
txyyh 325d6df2d3 Add thanks (#106) 2022-07-20 07:57:02 +08:00
txyyh 3ba08bf003 Auto-gen releases discussion (#105) 2022-07-20 07:56:49 +08:00
tarsin 86d00cc0d1 Use mdc's slider 2022-07-19 22:17:40 +08:00
tarsin 4ee518451c Release 1.7.28.3 2022-07-19 19:42:10 +08:00
tarsin 7b167b1612 Check to our xz fork 2022-07-19 19:01:03 +08:00
txyyh 7f3965bb08 Regen repo link in app (#103) 2022-07-19 17:27:00 +08:00
txyyh 49ea156ea8 Regen repo link (#102) 2022-07-19 17:20:02 +08:00
tarsin 90c7f9fd98 Re-checkout upstream 2022-07-19 17:02:22 +08:00
tarsin 1604885596 Optimize size 2022-07-19 16:37:48 +08:00
NekoInverter f0fbf2f46e Open app info when cannot open open by default 2022-07-19 16:37:48 +08:00
txyyh 3ab563e25d Require SoC and device model in bug report template (#101)
* Require device model on bug report

* Require SoC model on bug report
2022-07-19 12:27:54 +08:00
tarsin 1bdb52a096 kotlinize 2022-07-19 12:16:58 +08:00
tarsin 029401288e Rename .java to .kt 2022-07-19 12:16:57 +08:00
tarsin b84daa45c5 kotlinize 2022-07-19 12:02:07 +08:00
tarsin ba3fe869d8 Rename .java to .kt 2022-07-19 12:02:07 +08:00
tarsin 74494848d3 kotlinize 2022-07-19 11:49:51 +08:00
tarsin fa0a72e159 Rename .java to .kt 2022-07-19 11:49:51 +08:00
tarsin 30c408a27f kotlinize 2022-07-19 11:41:49 +08:00
tarsin 4e9a8be076 Rename .java to .kt 2022-07-19 11:41:49 +08:00
tarsin e3a87d44f4 Gallery info card tweaks 2022-07-18 22:15:28 +08:00
tarsin 1387b7acf9 Revert "Update proguard-rules.pro"
This reverts commit 77e5c3a347.
2022-07-18 22:11:22 +08:00
tarsin 3df6fed3ac Only use kotlin 1.7.10 2022-07-18 21:17:36 +08:00
tarsin 30d11e92c6 Regen gradle wrappers 2022-07-18 21:05:13 +08:00
tarsin 77e5c3a347 Update proguard-rules.pro 2022-07-18 21:03:08 +08:00
tarsin 2ddb26b1aa app: Remove unused res 2022-07-18 20:52:52 +08:00
tarsin 191db14398 app: Do code cleanup 2022-07-18 20:47:04 +08:00
tarsin 9dcc24f4d9 Make renovate bot happy 2022-07-18 20:44:00 +08:00
tarsin 05d29f4cfd Update dependencies 2022-07-18 20:19:09 +08:00
tarsin f257c936ba Use fwb's glSurfaceView 2022-07-18 16:57:43 +08:00
tarsin c2d8cf8b47 Use fwb's html parser
* Drop tagsoup as well

* Our minsdk is 26, no need to concern about api compatibility below 24 then
2022-07-18 16:34:10 +08:00
txyyh 7cfbfb5db5 Update screenshots (#97) 2022-07-17 16:53:16 +08:00
tarsin 9d00e47f74 Refactor gallery comment to adapt non-divider recyclerview 2022-07-17 15:55:58 +08:00
tarsin 452b373f62 Don't use dividers
* That's so fucking ugly
2022-07-17 15:33:57 +08:00
tarsin 8fa04644c4 Convert filter add dialog's spinner to dropdownmenu 2022-07-17 15:06:44 +08:00
tarsin 67024486e7 Rename .java to .kt 2022-07-17 15:06:44 +08:00
txyyh e1be24053b Version Bump (#94)
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-17 00:25:33 +08:00
txyyh 5ddf2b0d8f CI for pull request (#95) 2022-07-17 00:25:20 +08:00
tarsin 0802b0b4c6 Drop NekoInverter's email 2022-07-16 07:20:12 +08:00
tarsin 2a709e6190 Make all fragmentPref use fade anim 2022-07-15 13:34:31 +08:00
tarsin 156e4f990d Workaround for preference anim 2022-07-15 13:12:30 +08:00
tarsin 07c1737597 Drop takisoft's preferenceFix 2022-07-15 11:24:53 +08:00
tarsin a3f61ab4ef Remove some workground 2022-07-15 07:43:14 +08:00
tarsin 7aa3eff23f Fix fab size 2022-07-15 07:39:58 +08:00
renovate[bot] a79375843b Update dependency com.google.android.material:material to v1.7.0-alpha03 (#93)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-15 06:53:50 +08:00
renovate[bot] 1b50ce31f5 Update dependency gradle to v7.5 (#91)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-15 06:53:38 +08:00
tarsin 167fe89908 Version Bump
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-14 11:20:29 +08:00
tarsin 892b3ece41 Fix name shadow 2022-07-14 11:19:10 +08:00
tarsin 6dfa056e41 Use polly optimization
* Polly is always exist in ndk
2022-07-14 11:08:39 +08:00
tarsin 1a8afd86be Use llvm's Link Time Optimization 2022-07-14 09:51:47 +08:00
tarsin e0ef18659b We have no cpp things 2022-07-14 09:26:37 +08:00
tarsin 52be469b2d Optimize size 2022-07-14 09:24:47 +08:00
tarsin 6895289ec8 Upgrade to ndk r25 2022-07-14 08:37:57 +08:00
tarsin 3e86cbcf60 Convert a spinner to m3AutoCompleteTextInputLayout 2022-07-13 23:45:12 +08:00
tarsin 536d294f7b Rename .java to .kt 2022-07-13 23:45:12 +08:00
tarsin 8e7888df35 Use rikkax's correct m3Switch temporarily 2022-07-13 21:04:58 +08:00
tarsin 79703ce0dd Make simplemenuPreference md3 2022-07-13 20:56:25 +08:00
tarsin 25aa08d8e4 Use AppcompatSpinner 2022-07-13 20:06:25 +08:00
hathlife 35a77cd03d Add TV layout for initial wizard scenes. (#90)
* fix typo

* Add TV layout for initial setting scenes.

* Add credits
2022-07-13 07:36:55 +08:00
renovate[bot] 1198927d85 Update dependency org.jboss.forge.roaster:roaster-api to v2.26.0.Final (#87)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-11 22:49:38 +08:00
renovate[bot] f1c5e7660e Update dependency org.jboss.forge.roaster:roaster-jdt to v2.26.0.Final (#88)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-11 22:47:47 +08:00
txyyh 0f99403702 Use webp instead 2022-07-11 00:19:38 +08:00
tarsin 10170a274b Only need unlock when process is paused 2022-07-10 22:08:11 +08:00
tarsin 47630e6620 Implement #81 2022-07-10 20:05:13 +08:00
tarsin bb5799b414 Fix #86 2022-07-10 19:23:36 +08:00
renovate[bot] 2110e1b102 Update dependency dev.rikka.rikkax.core:core-ktx to v1.4.1 (#83)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-10 13:10:51 +08:00
hathlife 3dfbd102b4 Add Entry for Android TV launchers (#82) 2022-07-10 10:14:04 +08:00
renovate[bot] 0ba6500625 Update plugin org.jetbrains.kotlin.android to v1.7.10 (#80)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-08 00:06:29 +08:00
tarsin 2e0fdcf507 Version Bump
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-07 10:36:16 +08:00
tarsin b72f5315fe Fix side drawer hint 2022-07-07 10:34:10 +08:00
tarsin 2c3bafdac3 Revert "Initial archive export stub"
This reverts commit ce8da252c3.
2022-07-07 10:30:25 +08:00
tarsin 6d0b55ac26 Drop res for api below Oreo 2022-07-07 09:24:45 +08:00
tarsin e0bd573988 Gallery detail card layout refactor 2022-07-07 08:51:00 +08:00
tarsin 3de3046b8a Fix #79 2022-07-07 08:06:07 +08:00
tarsin 7ccc8dd6b6 Tag color tweaks, fix #78 2022-07-07 07:59:17 +08:00
renovate[bot] 1d0ae20920 Update dependency org.jsoup:jsoup to v1.15.2 (#76)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-04 21:05:17 +08:00
txyyh cbee05d35b Place FOSSA picture (#75) 2022-07-02 15:27:56 +08:00
fossabot ad937488a6 Add license scan report and status (#74)
Signed off by: fossabot <badges@fossa.com>
2022-07-02 13:40:45 +08:00
tarsin 9f53f1fe4d Fix for issue that can't switch to default locale when custom locale is set 2022-07-01 22:18:00 +08:00
txyyh b33dea3cb0 Small README changes (#73) 2022-07-01 20:32:03 +08:00
txyyh 35d7bf6e0b Rewrite File (#72) 2022-06-30 18:51:21 +08:00
txyyh 9af8c85994 Change LICENSE to GPLv3 (#70)
* Change LICENSE file to GPLv3

* Change README LICENSE NOTICE to GPLv3

* Add NOTICE Header
2022-06-30 07:31:41 +08:00
renovate[bot] ea1233f474 Update dependency androidx.fragment:fragment to v1.5.0 (#71)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-30 01:31:12 +08:00
renovate[bot] a9fe136c55 Update dependency com.squareup.okhttp3:okhttp to v5.0.0-alpha.10 (#69)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 14:13:44 +08:00
tarsin 0f5d2abaef Update detail scene chip icon 2022-06-26 21:48:50 +08:00
tarsin 4ce4983ac8 Fix #67 2022-06-26 19:07:14 +08:00
tarsin 9cc4e31ba1 Fix up 2022-06-26 11:13:30 +08:00
tarsin b9fb515832 Fix pure black theme 2022-06-25 21:26:13 +08:00
tarsin 46ad431a5c Upgrade androidx activity to latest 2022-06-25 18:56:20 +08:00
tarsin c2e6711575 Fill navbar divider color for pad 2022-06-25 18:43:03 +08:00
tarsin c6d2a61652 Should not use rikka's appcompat
* We need latest appcompat for materialSwitch
2022-06-25 18:32:48 +08:00
tarsin e3bf0e8341 Drop doh 2022-06-25 07:46:13 +08:00
tarsin 70fa2eb821 Pop stack when backpressed and stack not empty 2022-06-25 01:45:30 +08:00
tarsin afe94b5dba Emergency fix for quit when stack is not empty
Fixes:https://github.com/asuka-mio/Ehviewer/commit/10caf7bbd8472dabe616733ad92aafaf89b2a9ee
2022-06-25 01:31:49 +08:00
tarsin 846f83918b Upgrade to okhttp3 v5 2022-06-24 23:37:00 +08:00
tarsin 27caf83ef3 Drop unused code 2022-06-24 23:29:22 +08:00
txyyh 24761fe05b Fix wrong link (#64) 2022-06-24 23:00:43 +08:00
tarsin 3726530af9 Convert disklrucache to okio impl
* Idk whether okio's sink->outstream will cause perf loss,
  but this impl is obviously socket friendly and in maintenance
2022-06-24 21:41:55 +08:00
tarsin 8e0f5dc697 Use framework's lrucache 2022-06-24 19:11:11 +08:00
tarsin 24d6b47941 Delete NOTICE.html 2022-06-24 18:05:05 +08:00
tarsin 7fd3ae0702 Update README.md 2022-06-24 18:00:46 +08:00
tarsin 10caf7bbd8 Allow cached on background 2022-06-23 12:06:05 +08:00
tarsin 3ee63bdd47 Add some haptic effect 2022-06-23 11:27:42 +08:00
tarsin 2e4f6398f6 androidx annotation should be compileOnly 2022-06-23 09:12:15 +08:00
tarsin 542eeda11c Drop zstd archive format support 2022-06-23 09:06:30 +08:00
tarsin 0ced054631 Drop edgeEffect
* This md2 but not md3

* We have no reason to reinvent the wheel
2022-06-23 08:16:40 +08:00
tarsin 2e74222244 TagGroup's color should be Secondary 2022-06-23 07:53:56 +08:00
童心音乐会 9f23e4d7b3 Merge pull request #62 from txyyh/master
Complete Issue Template
2022-06-23 03:51:18 +08:00
txyyh 05abc83e6e Add Q&A check option 2022-06-23 03:49:12 +08:00
txyyh 18ea016014 Switch BR to yml 2022-06-23 03:49:07 +08:00
童心音乐会 b04264793b Merge pull request #57 from txyyh/feature/issue
Rewrite FR issue template
2022-06-23 01:48:40 +08:00
txyyh cb76649fdf Switch FR to yml 2022-06-23 01:41:16 +08:00
txyyh b19c9fb132 Switch to link 2022-06-23 01:41:16 +08:00
txyyh aa759f5187 Delete useless issue template 2022-06-23 01:41:16 +08:00
renovate[bot] ef504d5a71 Update dependency dev.rikka.rikkax.material:material to v2.5.0 (#59)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-22 19:51:09 +08:00
tarsin 8cfa98e971 Update README.md 2022-06-19 20:05:36 +08:00
renovate[bot]andRenovate Bot 4577168953 Update dependency androidx.annotation:annotation to v1.4.0 (#58)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-06-16 08:43:15 +08:00
tarsin ac74a30fcf Always cut and scale thumb on some scenes 2022-06-15 23:10:12 +08:00
童心音乐会 ca802940b4 Auto add label to issue (#56) 2022-06-15 17:20:15 +08:00
童心音乐会 923858caa9 Merge pull request #55 from txyyh/feature/readme
Fix photo edge color
2022-06-15 15:40:55 +08:00
txyyh 68ae37093a Fix photo edge color 2022-06-15 15:38:08 +08:00
童心音乐会 000aeb920e Change README (#52)
* Use the second largest Heading

* Use right icon

* Add more screenshots
2022-06-15 12:29:42 +08:00
tarsin ce8da252c3 Initial archive export stub 2022-06-14 09:48:48 +08:00
tarsin de6a664729 Drop libfuse 2022-06-14 09:06:42 +08:00
tarsin 12bf20f656 Increase pipe size 2022-06-14 08:47:41 +08:00
tarsin c58493726e Revert back to minsdk26 and fill debug info
* libarchive said "Pathname cannot be converted from UTF-16LE to current locale",
  this should be a libarchive compatibility problem, thus drop support below api26
2022-06-14 07:51:30 +08:00
tarsin 541e637df0 Fix some drawable's color 2022-06-13 18:59:27 +08:00
tarsin 9d570a91d2 Temporarily drop compose 2022-06-13 18:44:44 +08:00
tarsin d54fe52060 Downgrade minsdk to 23 2022-06-13 18:31:28 +08:00
tarsin 1eb86355cb Drop hardcoded dynamic color
* Fix crash on api<S as well
2022-06-13 14:32:21 +08:00
tarsin e48ecac3cd Downgrade minsdk to 26 2022-06-13 13:12:14 +08:00
tarsin 850a8fb35c Inline hippo's all libs
* These dependencies are outdated and lack of maintenance
2022-06-13 13:04:50 +08:00
tarsin edd1ec2cc1 Inline hippo's yorozuya-collect 2022-06-13 12:47:38 +08:00
tarsin 622d4055d9 Reformat code 2022-06-13 12:42:00 +08:00
tarsin 36aa37359b Inline hippo's beerbelly
* No more dependencies spam thanks
2022-06-13 12:39:34 +08:00
tarsin 7c44cd3ebb Inline hippo's yorozuya-thread
* No more dependencies spam thanks
2022-06-13 12:31:01 +08:00
tarsin 95a3581ce6 Upgrade buildtools to 33 2022-06-13 10:10:26 +08:00
Renovate Bot 41b27de058 Update dependency com.squareup.okhttp3:okhttp to v4.10.0 2022-06-13 00:13:17 +08:00
tarsin 0db262681b Merge pull request #50 from asuka-mio/renovate/com.squareup.okhttp3-okhttp-dnsoverhttps-4.x
Update dependency com.squareup.okhttp3:okhttp-dnsoverhttps to v4.10.0
2022-06-13 00:10:17 +08:00
Renovate Bot b75c74096c Update dependency com.squareup.okhttp3:okhttp-dnsoverhttps to v4.10.0 2022-06-12 15:38:42 +00:00
tarsin c68c67b192 Address a deprecated function 2022-06-10 09:22:16 +08:00
tarsin 9ffd0971ae Kotlin 1.7.0 2022-06-10 09:08:21 +08:00
tarsin b276f2f25f Bump targetSDK to 33
* Sadly I can't compile Tiramisu for my 10Ultra now, but ArchlinuxCN updates, thus does I
2022-06-10 08:48:07 +08:00
tarsin e36c77c450 Fab drawable's color should be colorOnSurface 2022-06-05 07:18:01 +08:00
tarsin 4c8cb4eb97 Don't stretch the image
* Even computer is not so easy to process that
2022-06-02 19:47:38 +08:00
tarsin 746abe91ec Gallerydetail refactor 2022-06-02 18:58:30 +08:00
tarsin df0dee36c2 Merge pull request #46 from asuka-mio/renovate/androidx.compose.material3-material3-1.x
Update dependency androidx.compose.material3:material3 to v1.0.0-alpha13
2022-06-02 15:03:47 +08:00
tarsin 5af07c5f07 Merge pull request #45 from asuka-mio/renovate/compose
Update compose to v1.2.0-beta03
2022-06-02 15:03:36 +08:00
tarsin ed5dd9af21 Merge pull request #47 from asuka-mio/renovate/androidx.core-core-1.x
Update dependency androidx.core:core to v1.8.0
2022-06-02 15:03:20 +08:00
tarsin d1cfafa944 Material3 more widgets 2022-06-02 14:55:24 +08:00
Renovate Bot b4428dc1dc Update dependency androidx.core:core to v1.8.0 2022-06-01 21:04:51 +00:00
Renovate Bot f649687b74 Update dependency androidx.compose.material3:material3 to v1.0.0-alpha13 2022-06-01 17:47:21 +00:00
Renovate Bot 905f327031 Update compose to v1.2.0-beta03 2022-06-01 17:47:18 +00:00
tarsin 12f857a936 Reformat Code 2022-05-30 09:23:41 +08:00
tarsin 01daf4a63e Inline hippo's glcomponents
* Replace android support with androidx as well
2022-05-30 09:20:02 +08:00
tarsin cf3769959f Rel
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-27 23:37:42 +08:00
tarsin 10e853f968 Build static library 2022-05-27 23:23:04 +08:00
tarsin ac6995be8d Setting preference wants to be material, too 2022-05-27 21:53:34 +08:00
tarsin 045d6f15a7 Workaround for mdc170alpha02
* See https://github.com/material-components/material-components-android/issues/2720
2022-05-27 21:35:36 +08:00
tarsin 2d16f1cb57 Convert to materialswitch 2022-05-27 21:29:04 +08:00
tarsin ebe1d451b0 Drop build guide
* No longer force cmake version
2022-05-27 20:27:56 +08:00
tarsin 7faa4bea0d Add zstd to Credits 2022-05-27 20:26:53 +08:00
tarsin 3785d3130c Inline yorozuya 2022-05-27 00:48:51 +08:00
tarsin 9daa1c27c4 Drop rikkax core 2022-05-27 00:30:05 +08:00
tarsin 67c722ed43 Revert "Drop rikkax core"
This reverts commit 7a64fd4c11.
2022-05-27 00:27:25 +08:00
tarsin 3c8f1809aa Revert "Drop rikkax appcompat"
This reverts commit f9aad6ce9c.
2022-05-27 00:27:14 +08:00
tarsin c5a3059e31 Revert "Revert "Drop rikkax appcompat""
This reverts commit c7ffdbec93.
2022-05-27 00:26:59 +08:00
tarsin f305a59880 Revert "Drop rikkax"
This reverts commit 87d284ad97.
2022-05-27 00:26:48 +08:00
tarsin aa18e5f2f1 Revert "Drawable color fix"
This reverts commit 2f3f3d6480.
2022-05-27 00:26:35 +08:00
tarsin ac25b95eae Revert "Tag color fix"
This reverts commit a92fa304f1.
2022-05-27 00:26:09 +08:00
tarsin a92fa304f1 Tag color fix 2022-05-26 23:00:45 +08:00
tarsin 2f3f3d6480 Drawable color fix 2022-05-26 22:23:00 +08:00
tarsin 0d108393fc Don't set cmake version 2022-05-26 19:57:39 +08:00
tarsin 8d02f8c6a3 Upgrade cmake 2022-05-26 19:51:06 +08:00
tarsin 87d284ad97 Drop rikkax
* TODO: Reimplement some functions

* Locale set is not needed as Android 13 will land

* Don't release until all fixed
2022-05-26 19:37:15 +08:00
tarsin c7ffdbec93 Revert "Drop rikkax appcompat"
This reverts commit f9aad6ce9c.
2022-05-26 18:16:34 +08:00
tarsin f9aad6ce9c Drop rikkax appcompat 2022-05-26 18:10:49 +08:00
tarsin 7a64fd4c11 Drop rikkax core 2022-05-26 18:09:17 +08:00
tarsin 909af0cbed Upgrade material components 2022-05-26 17:51:02 +08:00
tarsin 34db6fc79f Merge pull request #43 from asuka-mio/renovate/com.android.application-7.x
Update plugin com.android.application to v7.2.1
2022-05-26 00:36:48 +08:00
Renovate Bot b025f150c9 Update plugin com.android.application to v7.2.1 2022-05-25 16:29:11 +00:00
tarsin fea6b38c1e Drop rikkax's layoutinflater
* This is unused
2022-05-25 17:31:56 +08:00
tarsin e2a03427f9 Drop rikkax's inset
* This is unused
2022-05-25 17:30:50 +08:00
tarsin 654cb5fdd5 Drop rikkax's recyclerview fix
* Now that we are Android S and above, we don't need these fix anymore
2022-05-25 17:28:04 +08:00
tarsin 333419b716 Drop dynanim
* Unused and unmaintained for three years
2022-05-25 17:16:33 +08:00
tarsin c66b1e4622 Drop a class that no longer used 2022-05-25 16:41:30 +08:00
tarsin ac55e2f8fc Version bump
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-25 16:31:27 +08:00
tarsin 24718e7d25 Add my name to author 2022-05-25 16:30:50 +08:00
tarsin 72e1fe9621 Rename packagename 2022-05-25 16:19:59 +08:00
tarsin 0c7bf96bf5 No "require unlock" available when keyguard is not enabled 2022-05-25 15:55:54 +08:00
tarsin f1dde6c8bc Rename .java to .kt 2022-05-25 15:55:54 +08:00
tarsin 3177a8352f Drop unused res 2022-05-25 15:30:01 +08:00
tarsin 3cb024b7f5 Do code reformat 2022-05-25 15:25:57 +08:00
tarsin 0a09d16500 Drop LockPatternView, migrate to system's provider 2022-05-25 15:24:35 +08:00
tarsin a79b1ad785 Rename .java to .kt 2022-05-25 15:24:35 +08:00
tarsin 4388c53929 Clean primary color
* No hardcode primary color anymore
2022-05-25 14:08:57 +08:00
tarsin 02f000d37b Merge pull request #41 from txyyh/feature/checkbox
Use checkbox instead
2022-05-24 23:44:32 +08:00
txyyh 7259e5e315 Use checkbox instead
It's clear that button will be too big when switch to m3 button, and radio button is not suitable, so that's it.
2022-05-24 23:01:43 +08:00
tarsin 7ce744b5af Fix #39 2022-05-24 21:34:37 +08:00
tarsin 57f21c8d39 Version bump
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-24 18:19:11 +08:00
tarsin 07f27a8438 screenshot update 2022-05-24 18:16:56 +08:00
tarsin 4f4dc55b4e itemcard layout tweaks 2022-05-24 18:15:33 +08:00
tarsin 8e9cda8e93 update screenshot 2022-05-24 17:37:46 +08:00
tarsin d5cfabd73e Only check this when longpressed on chip 2022-05-24 17:12:05 +08:00
tarsin 4f1b371e75 Drop fuse-archive
* This glue can't work with latest libfuse and require c++ STL for no reason, drop it and try find better impl
2022-05-24 16:59:07 +08:00
tarsin 4d8b50912d Version bump
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-24 16:52:50 +08:00
tarsin 660c717cfb Make elf32 release 2022-05-24 16:51:37 +08:00
tarsin 70d3d9a5b2 Update jconfigint.h 2022-05-24 16:35:37 +08:00
tarsin 464da25ca1 Update android.yml 2022-05-24 16:08:58 +08:00
tarsin c2795120e1 Update CMakeLists.txt 2022-05-24 16:00:51 +08:00
tarsin b10fb46f1f Update build.gradle 2022-05-24 15:48:38 +08:00
tarsin e39897176a Update build.gradle 2022-05-24 15:47:45 +08:00
tarsin 6e7e6fab74 Import fuse-archive 0.1.11
* From https://github.com/google/fuse-archive
2022-05-24 11:33:41 +08:00
tarsin 94009b6829 Build libfuse 2022-05-24 11:09:19 +08:00
tarsin 289217fefc Add libfuse 3.11.0 2022-05-24 10:02:26 +08:00
tarsin 97feab1203 Rename native libs, drop a rikka util 2022-05-24 09:23:05 +08:00
tarsin a9a3b20eb3 Make all lib shared 2022-05-23 21:22:39 +08:00
tarsin 7af1e168a4 Bump Version
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-23 20:46:46 +08:00
tarsin c8bf34e0d7 Switch to md3 switch 2022-05-23 20:46:09 +08:00
tarsin 86d2282cd6 native: Reformat code 2022-05-23 20:04:04 +08:00
tarsin cfdb23d6dc app: Reformat code 2022-05-23 19:59:40 +08:00
tarsin 1874ddd15b Add nativelib version check 2022-05-23 19:58:16 +08:00
tarsin a004c308cc Rename .java to .kt 2022-05-23 19:58:16 +08:00
tarsin 863337c789 Support fskip for better decompress perf
* Actually the whole impl can't be more sucks, will import algorithm to manage buffer and read strategy further
2022-05-23 17:34:59 +08:00
tarsin 64d91eed33 Upgrade libjpeg-turbo to 2.1.3 2022-05-23 14:55:15 +08:00
tarsin d0c3a3e14d Refactor cmake 2022-05-23 11:36:51 +08:00
tarsin 464636d87d Add zstd support 2022-05-23 11:28:28 +08:00
tarsin 0b0a1f456f Reduce supported ext
* hippo's legacy glgallery only support these four extensions, further support need we migrate to coil and drop glgallery, in progress
2022-05-23 08:40:15 +08:00
tarsin 302030ca65 Update README.md 2022-05-23 08:28:38 +08:00
tarsin 3e63738542 Make a release
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-23 08:21:14 +08:00
tarsin d6823c9c8e Checkout to our newly patched libarchive 2022-05-23 08:04:59 +08:00
tarsin 6bc3866660 Build lzma
FIXME: Don't hack!!
https://github.com/asuka-mio/libarchive/commit/588598490ad0a80e44554ed5e608a9f9878220c0
2022-05-23 08:02:20 +08:00
tarsin f41f4dd8ba Support 7z read 2022-05-23 07:31:58 +08:00
tarsin cde39812ce Update proguard-rules.pro 2022-05-23 01:12:58 +08:00
tarsin 03c2abc901 Merge pull request #34 from txyyh/feature/Button
Use Material3 Button instead
2022-05-23 00:59:14 +08:00
tarsin 7646334525 Don't obfuscate native code 2022-05-23 00:59:00 +08:00
txyyh f8d1cd3fab Use Material3 Button instead 2022-05-23 00:47:28 +08:00
tarsin 4ea94be64a Update releases.yml 2022-05-23 00:44:56 +08:00
tarsin dc0470e75a Bump Version
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-23 00:34:33 +08:00
tarsin 5d49ecdd7d Add lzma xz v5.2.5 2022-05-23 00:29:01 +08:00
tarsin ef2441116d Use more friendly suffix recogniser and support all image file 2022-05-22 23:17:35 +08:00
tarsin 207909c7be Initial libarchive simple support 2022-05-22 22:27:31 +08:00
tarsin ddb3dd2d95 Merge pull request #33 from txyyh/feature/buildguide
Add build guide
2022-05-22 02:17:38 +08:00
txyyh 8bb980fe97 Add build guide 2022-05-22 01:55:38 +08:00
tarsin 3c3a201ed8 Merge pull request #30 from txyyh/feature/README
Rewrite README header
2022-05-22 01:31:13 +08:00
tarsin 81bb37e929 Merge pull request #31 from asuka-mio/renovate/org.jboss.forge.roaster-roaster-api-2.x
Update dependency org.jboss.forge.roaster:roaster-api to v2.25.0.Final
2022-05-22 01:30:54 +08:00
Renovate Bot ae34690eb2 Update dependency org.jboss.forge.roaster:roaster-api to v2.25.0.Final 2022-05-21 17:30:16 +00:00
tarsin 285b0f5a69 Merge pull request #32 from asuka-mio/renovate/org.jboss.forge.roaster-roaster-jdt-2.x
Update dependency org.jboss.forge.roaster:roaster-jdt to v2.25.0.Final
2022-05-22 01:29:43 +08:00
Renovate Bot a5327fce07 Update dependency org.jboss.forge.roaster:roaster-jdt to v2.25.0.Final 2022-05-21 15:11:11 +00:00
txyyh a4b750f9cd Add description title 2022-05-21 21:58:53 +08:00
txyyh 943344d4bb Rewrite shields 2022-05-21 21:58:50 +08:00
txyyh 79f1b92960 Rewrite icon header 2022-05-21 21:26:48 +08:00
tarsin 575ae38a28 Re-checkout 2022-05-21 20:46:25 +08:00
tarsin f90e2a5c80 Checkout to our libarchive 2022-05-21 20:34:06 +08:00
tarsin 4d9b2877fe Build libarchive 2022-05-21 20:08:03 +08:00
tarsin 4a955947a2 Add libarchive 2022-05-21 19:41:09 +08:00
tarsin 7307c136ee Drop useless native part 2022-05-21 17:24:03 +08:00
tarsin 657e1b1d0f Drop a7zip for libarchive integrate 2022-05-21 17:04:27 +08:00
tarsin 43edf11275 Bump Version
EhViewer / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-05-21 00:10:34 +08:00
tarsin 32ec4c6715 Merge pull request #29 from txyyh/feature/AutoSign
AutoSign and Releases
2022-05-21 00:04:58 +08:00
txyyh 84ac57d69e Releases with tag 2022-05-20 23:57:51 +08:00
txyyh 328e7745b7 Add step name 2022-05-20 23:23:14 +08:00
txyyh ccf22eaef4 Upload right artifact 2022-05-20 23:23:14 +08:00
txyyh c89d42f368 Use keystore in project 2022-05-20 23:23:14 +08:00
txyyh 41192ea7f5 Format 2022-05-20 23:23:14 +08:00
tarsin 7840987a7c native cmake tweaks 2022-05-20 22:59:23 +08:00
tarsin fcbe0a7f55 layout tweaks 2022-05-20 20:33:37 +08:00
tarsin f12894c95d Extract help message from add action 2022-05-20 20:32:15 +08:00
tarsin 40f139fda2 Merge pull request #28 from txyyh/master
Change gallery_detail button theme
2022-05-20 17:32:01 +08:00
txyyh b8507a371e Button side-by-side 2022-05-20 14:32:57 +08:00
txyyh e5a71ce906 Use TextButton instead 2022-05-20 14:32:57 +08:00
txyyh f9e708fe1e Disable bold text 2022-05-20 14:32:57 +08:00
txyyh 50ba6f2bd2 Use elevated instead 2022-05-20 14:32:57 +08:00
tarsin 2484371f8c Import compose activity 2022-05-20 13:42:02 +08:00
tarsin ba675c4212 Use SwitchPreferenceCompat 2022-05-20 13:40:52 +08:00
tarsin 6f9bc3135a Import Jetpack Compose 2022-05-20 13:04:54 +08:00
tarsin 4475d4d018 Search layout tweaks 2022-05-20 12:31:00 +08:00
tarsin 08e45db49d Update README.md 2022-05-20 01:34:25 +08:00
tarsin 85c76065a5 Delete codeql-analysis.yml 2022-05-20 01:28:53 +08:00
tarsin 13582c3f1b Create codeql-analysis.yml 2022-05-20 01:24:27 +08:00
482 changed files with 34677 additions and 19109 deletions

No files matched your search

@@ -1,20 +0,0 @@
---
name: 功能申请 / Feature request
about: 有个想法 / Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**这个功能是用来解决问题的吗? / Is your feature request related to a problem? Please describe.**
描述这个问题。
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**描述你想要的解决方法 / Describe the solution you'd like**
描述应通过何种行为来解决这个问题。
A clear and concise description of what you want to happen.
**备注 / Additional context**
还有别的想说的吗?
Add any other context or screenshots about the feature request here.
@@ -1,41 +0,0 @@
---
name: Bug 反馈 / Bug report
about: 出问题了 / Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**简略描述 / Describe the bug**
大概说下是什么问题。
A clear and concise description of what the bug is.
**如何重现 / To Reproduce**
重现的步骤:
1. 打开什么什么
2. 点击什么什么
3. 出现什么什么问题
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**预期行为 / Expected behavior**
应该是什么样子的?
A clear and concise description of what you expected to happen.
**截图 / Screenshots**
能截图的话就发截图,请注意风序良俗。
If applicable, add screenshots to help explain your problem.
**设备型号与 Android 版本 / Device model and Android version**
- 设备型号:【比如 Pixel 6 XXXL】
- Android 版本:【比如 Android 12.1】
- Device model: [e.g. Pixel 6 XXXL]
- Android version: [e.g. Android 12.1]
**备注 / Additional context**
还有什么想说的?
Add any other context about the problem here.
+144
View File
@@ -0,0 +1,144 @@
name: Bug 反馈 / Bug report
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:
label: 复现步骤 / Step to reproduce
description: |
在此处写下复现的方式
Describe how to reproduce here
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id : expected
attributes:
label: 预期行为 / Expected behaviour
description: |
在此处说明正常情况下应用的预期行为
Describe what should happened here
placeholder: |
它应该XXX...
It should be ...
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为 / Actual Behaviour
description: |
在此处描绘应用的实际行为,最好附上截图
Describe what actually happened here, screenshots is better
placeholder: |
实际上它XXX...
Actually it ...
[截图] / [Screenshots]
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: 1.6.9.13
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
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
- type: input
id: system
attributes:
label: Android 系统版本 / Android version
description: Android 分支名称 + 版本号 / AOSP fork name + version code
placeholder: MIUI 12.5, ArrowOS 12.1
validations:
required: true
- type: input
id: device
attributes:
label: 设备型号 / Device model
description: 在此填入设备型号 / Put device model here
placeholder: OnePlus 7 Pro, Xiaomi 12 Ultra
validations:
required: true
- type: input
id: SoC
attributes:
label: SoC 型号 / Soc model
description: 在此填入 SoC 型号 / Put Soc model here
placeholder: 骁龙 8+ Gen 1, Snapdragon 8+ Gen 1
validations:
required: true
- type: textarea
id: log
attributes:
label: 应用日志 / App logs
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: true
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Verify steps
description: |
请确认您已经遵守所有必选项
Please ensure you have obtained all needed options
options:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
required: true
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
required: true
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
@@ -0,0 +1,94 @@
name: 功能请求 / Feature request
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:
label: 需求 / Requirement
description: |
在此处描述您的需求。这通常会是一个您想要的功能
Ddescribe what you need here.
placeholder: |
我需要 XXX 功能 ...
I want ABC feature ...
validations:
required: true
- type: textarea
id: impl
attributes:
label: 建议实现 / Suggested implements
description: |
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
placeholder: |
建议在 XX 处添加 XX ...
I recommend add ABC feature to DEF...
图片(如果有)/ Photos (if exists)
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: 1.6.9.13
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
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
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Verify steps
description: |
请确认您已经遵守所有必选项
Please ensure you have obtained all needed options
options:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
@@ -1,11 +0,0 @@
---
name: Sad Panda / 下载太慢 / Slow download speed
about: 所有其他问题 / All other questions
title: ''
labels: ''
assignees: ''
---
加入我们的 Telegram 群组,[https://t.me/ehviewer](https://t.me/ehviewer)。温柔[大小]姐姐手把手帮你解决问题。
Join our Telegram group, [https://t.me/ehviewer](https://t.me/ehviewer). Administrators will help you solve the problem.
-28
View File
@@ -1,28 +0,0 @@
name: EhViewer
on: push
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;24.0.8215888" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- uses: gradle/gradle-build-action@v2
- run: |
git submodule update --init --recursive
gradle assembleRelease
- uses: actions/upload-artifact@v3
with:
name: github-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
+58
View File
@@ -0,0 +1,58 @@
name: CI
on:
push:
branches:
- '*'
tags-ignore:
- "*"
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --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
uses: actions/upload-artifact@v3
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Update x86_64
uses: actions/upload-artifact@v3
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Update ARM32
uses: actions/upload-artifact@v3
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Update x86
uses: actions/upload-artifact@v3
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
+52
View File
@@ -0,0 +1,52 @@
name: Pull Request Check
on: [pull_request]
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Update x86_64
uses: actions/upload-artifact@v3
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Update ARM32
uses: actions/upload-artifact@v3
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Update x86
uses: actions/upload-artifact@v3
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
+51
View File
@@ -0,0 +1,51 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --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-A11-.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a-A11-.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64-A11-.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86-A11-.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a-A11-.apk
- name: Releases
uses: softprops/action-gh-release@v1
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/EhViewer-universal-A11-.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a-A11-.apk
app/build/outputs/apk/release/EhViewer-x86_64-A11-.apk
app/build/outputs/apk/release/EhViewer-x86-A11-.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a-A11-.apk
+12 -6
View File
@@ -1,12 +1,18 @@
[submodule "native/src/main/jni/a7zip/p7zip/p7zip"]
path = native/src/main/jni/a7zip/p7zip/p7zip
url = https://github.com/seven332/p7zip.git
[submodule "native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo"]
path = native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo
url = https://gitlab.com/NekoInverter/libjpeg-turbo.git
url = https://github.com/Ehviewer-Overhauled/libjpeg-turbo
[submodule "native/src/main/jni/image/giflib/giflib"]
path = native/src/main/jni/image/giflib/giflib
url = https://gitlab.com/NekoInverter/giflib.git
url = https://github.com/Ehviewer-Overhauled/giflib
[submodule "native/src/main/jni/image/libpng/libpng"]
path = native/src/main/jni/image/libpng/libpng
url = https://gitlab.com/NekoInverter/libpng.git
url = https://github.com/Ehviewer-Overhauled/libpng
[submodule "native/src/main/jni/libarchive"]
path = native/src/main/jni/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "native/src/main/jni/liblzma"]
path = native/src/main/jni/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "native/src/main/jni/nettle/nettle"]
path = native/src/main/jni/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+641 -169
View File
@@ -1,202 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Preamble
1. Definitions.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
The precise terms and conditions for copying, distribution and
modification follow.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
TERMS AND CONDITIONS
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
0. Definitions.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
"This License" refers to version 3 of the GNU General Public License.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
A "covered work" means either the unmodified Program or a work based
on the Program.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
1. Source Code.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
END OF TERMS AND CONDITIONS
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
APPENDIX: How to apply the Apache License to your work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
The Corresponding Source for a work in source code form is that
same work.
Copyright [yyyy] [name of copyright owner]
2. Basic Permissions.
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
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
http://www.apache.org/licenses/LICENSE-2.0
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
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.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
-1840
View File
File diff suppressed because it is too large. Load diff
+76 -31
View File
@@ -1,65 +1,110 @@
# EhViewer
<p align="right">
<strong>English<strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.7.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.7.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
![Icon](art/launcher_icon-web.png)
<h1 align="center">
<img src="art/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
这是一个 E-Hentai Android 平台的浏览器
<p align="center">
<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/1.7.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">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
</a>
</p>
已针对性适配 Material Design 3 及其动态取色系统
仅供 Android 12 及以上平台使用!
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/tree/1.7.x.x#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/tree/1.7.x.x#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/tree/1.7.x.x#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/tree/1.7.x.x#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/tree/1.7.x.x#license">
License
</a>
</h3>
</div>
An E-Hentai Application for Android.
# Description
with Material Design 3 and Dynamic Color Support.
EhViewer fork dedicated to lightweight and high-performance
Only Support Android S and above!
with Material Design 3 and Dynamic Color Support
# Download
[Github 下载](https://github.com/asuka-mio/Ehviewer/releases)
<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>
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
# Screenshot
![screenshot-01](art/screenshot-01.png)
![screenshot-02](art/screenshot-02.png)
# Thanks
本项目受到了诸多开源项目的帮助
Here is the libraries
- [AOSP](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)
- [material-design-icons](https://github.com/google/material-design-icons)
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
- [apng](http://apng.sourceforge.net/)
- [giflib](http://giflib.sourceforge.net)
- [greenDAO](https://github.com/greenrobot/greenDAO)
- [jsoup](https://github.com/jhy/jsoup)
- [libjpeg-turbo](http://libjpeg-turbo.virtualgl.org/)
- [libjpeg-turbo](https://libjpeg-turbo.org/)
- [libpng](http://www.libpng.org/pub/png/libpng.html)
- [okhttp](https://github.com/square/okhttp)
- [roaster](https://github.com/forge/roaster)
- [ShowcaseView](https://github.com/amlcurran/ShowcaseView)
- [Slabo](https://github.com/TiroTypeworks/Slabo)
- [TagSoup](http://home.ccil.org/~cowan/tagsoup/)
- [libarchive](http://www.libarchive.org/)
- [XZ Utils](https://tukaani.org/xz/)
# License
Copyright (C) 2014-2019 Hippo Seven
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022 Tarsin Norbin
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
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.
http://www.apache.org/licenses/LICENSE-2.0
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.
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.
ic_launcher 图标为 Hippo Seven 所有,所有权利保留
You should have received a copy of the GNU General Public License along with EhViewer. If not, see <https://www.gnu.org/licenses/>.
+82 -65
View File
@@ -5,8 +5,8 @@ plugins {
}
android {
compileSdkVersion 32
buildToolsVersion "32.0.0"
compileSdkVersion 33
buildToolsVersion "33.0.0"
Properties localProperties = new Properties()
if (project.rootProject.file('local.properties').exists()) {
@@ -17,27 +17,26 @@ android {
abi {
enable = true
reset()
include 'arm64-v8a', 'x86_64'
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
universalApk = true
}
}
signingConfigs {
if (localProperties.getProperty("storeFile") != null) {
sign {
storeFile file(localProperties.getProperty("storeFile"))
storePassword localProperties.getProperty("storePassword")
keyAlias localProperties.getProperty("keyAlias")
keyPassword localProperties.getProperty("keyPassword")
}
sign {
storeFile file(signStoreFile)
storePassword signStorePassword
keyAlias signkeyAlias
keyPassword signkeyPassword
}
}
defaultConfig {
applicationId "io.github.nekoinverter.ehviewer"
minSdkVersion 31
targetSdkVersion 32
versionCode 114518
versionName "1.7.26.9"
applicationId "moe.tarsin.ehviewer"
minSdkVersion 26
targetSdkVersion 33
versionCode 114537
versionName "1.7.31.0-alpha02"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
@@ -72,17 +71,11 @@ android {
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-rules.pro'
if (localProperties.getProperty("storeFile") != null) {
signingConfig signingConfigs.sign
}
signingConfig signingConfigs.sign
}
debug {
applicationIdSuffix ".debug"
if (localProperties.getProperty("storeFile") != null) {
signingConfig signingConfigs.sign
} else {
signingConfig signingConfigs.debug
}
signingConfig signingConfigs.debug
}
}
@@ -91,6 +84,7 @@ android {
java.srcDirs += 'src/main/java-gen'
}
}
namespace 'com.hippo.ehviewer'
}
@@ -99,56 +93,79 @@ tasks.withType(JavaCompile) {
}
dependencies {
implementation 'androidx.activity:activity:1.4.0'
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.core:core:1.7.0'
implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03'
implementation 'androidx.fragment:fragment:1.4.1'
def activity_version = "1.6.1"
implementation "androidx.activity:activity:$activity_version"
implementation "androidx.activity:activity-ktx:$activity_version"
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-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-alpha04"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
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-alpha02"
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.5.4"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_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"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'androidx.webkit:webkit:1.4.0'
implementation "androidx.transition:transition-ktx:1.4.1"
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
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'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.6.0'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
implementation 'com.github.seven332:beerbelly:0.1.4'
implementation 'com.github.seven332:glgallery:0.1.2'
implementation 'com.github.seven332:glview:0.1.0'
implementation 'com.github.seven332:glview-image:0.1.3'
implementation 'com.github.seven332:streampipe:0.1.0'
implementation 'com.github.seven332:tuxiang:0.1.6'
implementation 'com.github.seven332:unifile:9ec57bcd8f'
implementation 'com.github.seven332:yorozuya-thread:0.1.1'
implementation 'com.github.seven332:yorozuya-collect:0.1.4'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
implementation 'com.gitlab.NekoInverter:yorozuya:723dc867a9'
implementation 'dev.rikka.rikkax.appcompat:appcompat:1.4.1'
implementation 'dev.rikka.rikkax.core:core:1.4.0'
implementation 'dev.rikka.rikkax.insets:insets:1.2.0'
implementation 'com.google.android.material:material:1.8.0-alpha02'
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.material:material:2.4.0'
implementation 'dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1'
implementation 'dev.rikka.rikkax.widget:borderview:1.1.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'org.greenrobot:greendao:3.3.0'
implementation 'org.jsoup:jsoup:1.15.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
implementation projects.native
}
configurations.all {
resolutionStrategy {
force 'com.github.seven332:glgallery:25893283ca'
force 'com.github.seven332:glview:ba6aee61d7'
force 'com.github.seven332:glview-image:68d94b0fc2'
exclude group: 'com.github.seven332', module: 'okhttp'
exclude group: 'com.github.seven332', module: 'image'
exclude group: 'com.github.seven332', module: 'yorozuya'
exclude group: 'androidx.appcompat', module: 'appcompat'
}
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
}
Binary file not shown.
+34 -37
View File
@@ -14,17 +14,23 @@
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- Required to maintain app compatibility. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
@@ -41,11 +47,11 @@
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
android:theme="@style/AppTheme">
<activity
android:name="com.hippo.ehviewer.ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask"
@@ -56,6 +62,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
@@ -109,35 +120,21 @@
<data android:mimeType="application/x-7z-compressed" />
<data android:mimeType="application/x-rar-compressed" />
<data android:mimeType="application/x-zip-compressed" />
</intent-filter>
<data android:mimeType="application/x-tar" />
<data android:mimeType="application/x-xz" />
<data android:mimeType="application/gzip" />
<data android:mimeType="application/x-cbz" />
<data android:mimeType="application/x-cbr" />
<data android:mimeType="application/x-cbt" />
<data android:mimeType="application/x-cb7" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:pathPattern=".*\\.7z" />
<data android:pathPattern=".*\\.rar" />
<data android:pathPattern=".*\\.zip" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.7z" />
<data android:pathPattern=".*\\.rar" />
<data android:pathPattern=".*\\.zip" />
<!-- untested -->
<data android:mimeType="application/vnd.comicbook+zip" />
<data android:mimeType="application/vnd.comicbook-rar" />
<data android:mimeType="application/x-compressed-tar" />
<data android:mimeType="application/x-bzip-compressed-tar" />
<data android:mimeType="application/x-lzma-compressed-tar" />
<data android:mimeType="application/x-xz-compressed-tar" />
</intent-filter>
</activity>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

@@ -1,47 +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.app;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.ehviewer.R;
public class CheckBoxDialogBuilder extends MaterialAlertDialogBuilder {
private final CheckBox mCheckBox;
@SuppressLint("InflateParams")
public CheckBoxDialogBuilder(Context context, String message, String checkText, boolean checked) {
super(context);
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null);
setView(view);
setMessage(message);
mCheckBox = view.findViewById(R.id.checkbox);
mCheckBox.setText(checkText);
mCheckBox.setChecked(checked);
view.setOnClickListener(v -> mCheckBox.toggle());
}
public boolean isChecked() {
return mCheckBox.isChecked();
}
}
@@ -0,0 +1,45 @@
/*
* 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.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(
context: Context,
message: String?,
checkText: String?,
checked: Boolean
) : MaterialAlertDialogBuilder(
context
) {
private val mCheckBox: CheckBox
val isChecked: Boolean
get() = mCheckBox.isChecked
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)
setView(view)
setMessage(message)
mCheckBox = view.findViewById(R.id.checkbox)
mCheckBox.text = checkText
mCheckBox.isChecked = checked
view.setOnClickListener { mCheckBox.toggle() }
}
}
@@ -1,86 +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.app;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.ehviewer.R;
public class EditTextDialogBuilder extends MaterialAlertDialogBuilder implements EditText.OnEditorActionListener {
private final TextInputLayout mTextInputLayout;
private final EditText mEditText;
private AlertDialog mDialog;
@SuppressLint("InflateParams")
public EditTextDialogBuilder(Context context, String text, String hint) {
super(context);
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null);
setView(view);
mTextInputLayout = (TextInputLayout) view;
mEditText = view.findViewById(R.id.edit_text);
mEditText.setText(text);
mEditText.setSelection(mEditText.getText().length());
mEditText.setOnEditorActionListener(this);
mTextInputLayout.setHint(hint);
}
public EditText getEditText() {
return mEditText;
}
public String getText() {
return mEditText.getText().toString();
}
public void setError(CharSequence error) {
mTextInputLayout.setError(error);
}
@NonNull
@Override
public AlertDialog create() {
mDialog = super.create();
return mDialog;
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (mDialog != null) {
Button button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (button != null) {
button.performClick();
}
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,72 @@
/*
* 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.app
import android.content.Context
import android.content.DialogInterface
import android.view.KeyEvent
import android.view.LayoutInflater
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
class EditTextDialogBuilder constructor(
context: Context,
text: String?,
hint: String?
) : MaterialAlertDialogBuilder(
context
), OnEditorActionListener {
private val mTextInputLayout: TextInputLayout
val editText: EditText
private var mDialog: AlertDialog? = null
val text: String
get() = editText.text.toString()
fun setError(error: CharSequence?) {
mTextInputLayout.error = error
}
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean {
return if (mDialog != null) {
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
button?.performClick()
true
} else {
false
}
}
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)
setView(view)
mTextInputLayout = view as TextInputLayout
editText = view.findViewById(R.id.edit_text)
editText.setText(text)
editText.setSelection(editText.text.length)
editText.setOnEditorActionListener(this)
mTextInputLayout.hint = hint
}
}
@@ -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.app;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.ViewUtils;
public class ListCheckBoxDialogBuilder extends MaterialAlertDialogBuilder {
private final CheckBox mCheckBox;
private AlertDialog mDialog;
@SuppressLint("InflateParams")
public ListCheckBoxDialogBuilder(Context context, CharSequence[] items,
final OnItemClickListener listener, String checkText, boolean checked) {
super(context);
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null);
setView(view);
ListView listView = (ListView) ViewUtils.$$(view, R.id.list_view);
mCheckBox = (CheckBox) ViewUtils.$$(view, R.id.checkbox);
listView.setAdapter(new ArrayAdapter<>(getContext(), R.layout.item_select_dialog, items));
listView.setOnItemClickListener((parent, view1, position, id) -> {
if (listener != null) {
listener.onItemClick(ListCheckBoxDialogBuilder.this, mDialog, position);
}
mDialog.dismiss();
});
mCheckBox.setText(checkText);
mCheckBox.setChecked(checked);
}
public boolean isChecked() {
return mCheckBox.isChecked();
}
@NonNull
@Override
public AlertDialog create() {
mDialog = super.create();
return mDialog;
}
public interface OnItemClickListener {
void onItemClick(ListCheckBoxDialogBuilder builder, AlertDialog dialog, int position);
}
}
@@ -0,0 +1,65 @@
/*
* 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.app
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
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(
context
) {
private val mCheckBox: CheckBox
private var mDialog: AlertDialog? = null
val isChecked: Boolean
get() = mCheckBox.isChecked
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
interface OnItemClickListener {
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
}
init {
val view =
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
setView(view)
val listView = ViewUtils.`$$`(view, R.id.list_view) as ListView
mCheckBox = ViewUtils.`$$`(view, R.id.checkbox) as CheckBox
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
listView.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
mDialog?.dismiss()
}
mCheckBox.text = checkText
mCheckBox.isChecked = checked
}
}
@@ -0,0 +1,493 @@
/*
* Copyright (C) 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.beerbelly;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public abstract class BeerBelly<V> {
private static final String TAG = BeerBelly.class.getSimpleName();
private final boolean mHasMemoryCache;
private final boolean mHasDiskCache;
@Nullable
private MemoryCache<V> mMemoryCache;
@Nullable
private DiskCache<V> mDiskCache;
public BeerBelly(BeerBellyParams params) {
params.isValid();
mHasMemoryCache = params.hasMemoryCache;
mHasDiskCache = params.hasDiskCache;
if (mHasMemoryCache) {
initMemoryCache(params.memoryCacheMaxSize);
}
if (mHasDiskCache) {
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
}
}
private void initMemoryCache(int maxSize) {
mMemoryCache = new MemoryCache<>(maxSize, this);
}
private void initDiskCache(File cacheDir, int maxSize) {
// Set up disk cache
try {
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
} catch (IOException e) {
Log.e(TAG, "Can't create disk cache", e);
}
}
protected abstract int sizeOf(String key, V value);
protected abstract void memoryEntryAdded(V value);
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
protected abstract V read(@NonNull InputStreamPipe isPipe);
protected abstract boolean write(OutputStream os, V value);
@Nullable
public LruCache<String, V> getMemoryCache() {
return mMemoryCache;
}
@Nullable
public SimpleDiskCache getDiskCache() {
if (mDiskCache != null) {
return mDiskCache.mDiskCache;
} else {
return null;
}
}
/**
* Check if have memory cache
*
* @return true if have memory cache
*/
public boolean hasMemoryCache() {
return mHasMemoryCache;
}
/**
* Check if have disk cache
*
* @return true if have disk cache
*/
public boolean hasDiskCache() {
return mHasDiskCache;
}
/**
* Get memory cache usage size
*
* @return {@code -1} for no memory cache
*/
public int memorySize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.size();
} else {
return -1;
}
}
/**
* Get memory cache max size
*
* @return {@code -1} for no memory cache
*/
public int memoryMaxSize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.maxSize();
} else {
return -1;
}
}
/**
* Get disk cache usage size
*
* @return {@code -1L} for no memory cache
*/
public long diskSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.size();
} else {
return -1L;
}
}
/**
* Get disk cache max size
*
* @return {@code -1L} for no memory cache
*/
public long diskMaxSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.getMaxSize();
} else {
return -1L;
}
}
/**
* Get value from memory cache
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache
*/
public V getFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.get(key);
} else {
return null;
}
}
/**
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache or get error
*/
public V getFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.get(key);
} else {
return null;
}
}
/**
* Get value from memory cache and disk cache. If miss in memory cache and
* get in disk cache, it will put value from disk cache to memory cache.
*
* @param key the key to get value
* @return the value you get
*/
public V get(@NonNull String key) {
V value = getFromMemory(key);
if (value != null) {
// Get it in memory cache
return value;
}
value = getFromDisk(key);
if (value != null) {
// Get it in disk cache
putToMemory(key, value);
return value;
}
return null;
}
/**
* Put value to memory cache
*
* @param key the key
* @param value the value
* @return false if no memory cache
*/
public boolean putToMemory(@NonNull String key, @NonNull V value) {
if (mHasMemoryCache && mMemoryCache != null) {
memoryEntryAdded(value);
mMemoryCache.put(key, value);
return true;
} else {
return false;
}
}
/**
* Put value to disk cache
*
* @param key the key
* @param value the value
* @return false if no disk cache or get error
*/
public boolean putToDisk(@NonNull String key, @NonNull V value) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.put(key, value);
} else {
return false;
}
}
/**
* Put value to memory cache and disk cache
*
* @param key the key
* @param value the value
*/
public void put(@NonNull String key, @NonNull V value) {
putToMemory(key, value);
putToDisk(key, value);
}
public void removeFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.remove(key);
}
}
public void removeFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.remove(key);
}
}
public void remove(@NonNull String key) {
removeFromMemory(key);
removeFromDisk(key);
}
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.pull(key, os);
} else {
return false;
}
}
/**
* @param key the key
* @param is the input stream to store
* @return false if no disk cache or get error
*/
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.putRaw(key, is);
} else {
return false;
}
}
/**
* Evicts all of the items from the memory cache
*/
public void clearMemory() {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
/**
* Clear disk cache
*/
public void clearDisk() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.clear();
}
}
public void flush() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.flush();
}
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void clear() {
clearMemory();
clearDisk();
}
/**
* A holder class that contains cache parameters.
*/
public static class BeerBellyParams {
/**
* is memory cache available
*/
public boolean hasMemoryCache = false;
/**
* the maximum number of bytes the memory cache should use to store
*/
public int memoryCacheMaxSize = 0;
/**
* is disk cache available
*/
public boolean hasDiskCache = false;
/**
* the dir to store disk cache
*/
public File diskCacheDir = null;
/**
* the maximum number of bytes the disk cache should use to store
*/
public int diskCacheMaxSize = 0;
/**
* Check BeerBellyParams is valid
*/
public void isValid() throws IllegalStateException {
if (!hasMemoryCache && !hasDiskCache) {
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
}
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
}
if (hasDiskCache) {
if (diskCacheDir == null) {
throw new IllegalStateException("Disk cache dir can't be null.");
}
if (diskCacheMaxSize <= 0) {
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
}
}
}
}
private static class DiskCache<E> {
private static final int IO_BUFFER_SIZE = 8 * 1024;
private final SimpleDiskCache mDiskCache;
private final BeerBelly<E> mParent;
private final File mCacheDir;
private final int mMaxSize;
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
mDiskCache = new SimpleDiskCache(cacheDir, size);
mParent = parent;
mCacheDir = cacheDir;
mMaxSize = size;
}
public File getCacheDir() {
return mCacheDir;
}
public int getMaxSize() {
return mMaxSize;
}
public long size() {
return mDiskCache.size();
}
public void flush() {
mDiskCache.flush();
}
public void clear() {
mDiskCache.clear();
}
public E get(String key) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return null;
} else {
return mParent.read(isPipe);
}
}
public boolean put(String key, E value) {
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
BufferedOutputStream buffOut = null;
try {
osPipe.obtain();
OutputStream os = osPipe.open();
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
return mParent.write(os, value);
} catch (IOException e) {
return false;
} finally {
Util.closeQuietly(buffOut);
osPipe.close();
osPipe.release();
}
}
public boolean putRaw(String key, InputStream is) {
return mDiskCache.put(key, is);
}
public void remove(String key) {
mDiskCache.remove(key);
}
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return false;
} else {
try {
isPipe.obtain();
InputStream is = isPipe.open();
Util.copy(is, os);
return true;
} catch (IOException e) {
return false;
} finally {
isPipe.close();
isPipe.release();
}
}
}
}
private class MemoryCache<E> extends LruCache<String, E> {
public BeerBelly<E> mParent;
public MemoryCache(int maxSize, BeerBelly<E> parent) {
super(maxSize);
mParent = parent;
}
@Override
protected int sizeOf(String key, E value) {
return mParent.sizeOf(key, value);
}
@Override
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
}
}
}
@@ -0,0 +1,984 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.beerbelly;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
* entry has a string key and a fixed number of values. Each key must match
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
* accessible as streams or files. Each value must be between {@code 0} and
* {@code Integer.MAX_VALUE} bytes in length.
*
* <p>The cache stores its data in a directory on the filesystem. This
* directory must be exclusive to the cache; the cache may delete or overwrite
* files from its directory. It is an error for multiple processes to use the
* same cache directory at the same time.
*
* <p>This cache limits the number of bytes that it will store on the
* filesystem. When the number of stored bytes exceeds the limit, the cache will
* remove entries in the background until the limit is satisfied. The limit is
* not strict: the cache may temporarily exceed it while waiting for files to be
* deleted. The limit does not include filesystem overhead or the cache
* journal so space-sensitive applications should set a conservative limit.
*
* <p>Clients call {@link #edit} to create or update the values of an entry. An
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
* <li>When an entry is being <strong>created</strong> it is necessary to
* supply a full set of values; the empty value should be used as a
* placeholder if necessary.
* <li>When an entry is being <strong>edited</strong>, it is not necessary
* to supply data for every value; values default to their previous
* value.
* </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
* of values as they were before or after the commit, but never a mix of values.
*
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
* observe the value at the time that {@link #get} was called. Updates and
* removals after the call do not impact ongoing reads.
*
* <p>This class is tolerant of some I/O errors. If files are missing from the
* filesystem, the corresponding entries will be dropped from the cache. If
* an error occurs while writing a cache value, the edit will fail silently.
* Callers should handle other problems by catching {@code IOException} and
* responding appropriately.
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*
* The first five lines of the journal form its header. They are the
* constant string "libcore.io.DiskLruCache", the disk cache's version,
* the application's version, the value count, and a blank line.
*
* Each of the subsequent lines in the file is a record of the state of a
* cache entry. Each line contains space-separated values: a state, a key,
* and optional state-specific values.
* o DIRTY lines track that an entry is actively being created or updated.
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
* temporary files may need to be deleted.
* o CLEAN lines track a cache entry that has been successfully published
* and may be read. A publish line is followed by the lengths of each of
* its values.
* o READ lines track accesses for LRU.
* o REMOVE lines track entries that have been deleted.
*
* The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.
*/
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
@Override
public void write(int b) {
// Eat all writes silently. Nom nom.
}
};
/**
* This cache uses a single background thread to evict entries.
*/
final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
private final File directory;
private final File journalFile;
private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion;
private final int valueCount;
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<>(0, 0.75f, true);
private long maxSize;
private long size = 0;
private Writer journalWriter;
private int redundantOpCount;
/**
* To differentiate between old and current snapshots, each entry is given
* a sequence number each time an edit is committed. A snapshot is stale if
* its sequence number is not equal to its entry's sequence number.
*/
private long nextSequenceNumber = 0;
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
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 static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
/**
* Creates a new journal that omits redundant information. This replaces the
* current journal if it exists.
*/
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
try (Writer writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
}
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
/**
* Return {@code true} if it contains the key
*/
public synchronized boolean contain(String key) {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
return entry != null && entry.readable;
}
/**
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
* exist is not currently readable. If a value is returned, it is moved to
* the head of the LRU queue.
*/
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
// Open all streams eagerly to guarantee that we see a single published
// snapshot. If we opened streams lazily then the streams could come
// from different edits.
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
/**
* Returns an editor for the entry named {@code key}, or null if another
* edit is in progress.
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
/**
* Returns the directory where this cache stores its data.
*/
public File getDirectory() {
return directory;
}
/**
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
public synchronized long getMaxSize() {
return maxSize;
}
/**
* Changes the maximum number of bytes the cache can store and queues a job
* to trim the existing store, if necessary.
*/
public synchronized void setMaxSize(long maxSize) {
this.maxSize = maxSize;
executorService.submit(cleanupCallable);
}
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
/**
* Returns true if this cache has been closed.
*/
public synchronized boolean isClosed() {
return journalWriter == null;
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
/**
* Force buffered operations to the filesystem.
*/
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // Already closed.
}
for (Entry entry : new ArrayList<>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
*/
public void delete() throws IOException {
close();
Util.deleteContents(directory);
}
private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException("keys must match regex "
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
}
}
/**
* A snapshot of the values for an entry.
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
this.lengths = lengths;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public InputStream getInputStream(int index) {
return ins[index];
}
/**
* Returns the string value for {@code index}.
*/
public String getString(int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
/**
* Returns the byte length of the value for {@code index}.
*/
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
/**
* Edits the values for an entry.
*/
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
private Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Returns an unbuffered input stream to read the last committed value,
* or null if no value has been committed.
*/
public InputStream newInputStream(int index) {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
return null;
}
try {
return new FileInputStream(entry.getCleanFile(index));
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns the last committed value as a string, or null if no value
* has been committed.
*/
public String getString(int index) throws IOException {
InputStream in = newInputStream(index);
return in != null ? inputStreamToString(in) : null;
}
/**
* Returns a new unbuffered output stream to write the value at
* {@code index}. If the underlying output stream encounters errors
* when writing to the filesystem, this edit will be aborted when
* {@link #commit} is called. The returned output stream does not throw
* IOExceptions.
*/
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
/**
* Sets the value at {@code index} to {@code value}.
*/
public void set(int index, String value) throws IOException {
Writer writer = null;
try {
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value);
} finally {
Util.closeQuietly(writer);
}
}
/**
* Commits this edit so it is visible to readers. This releases the
* edit lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
/**
* Aborts this edit. This releases the edit lock so another edit may be
* started on the same key.
*/
public void abort() throws IOException {
completeEdit(this, false);
}
public void abortUnlessCommitted() {
if (!committed) {
try {
abort();
} catch (IOException ignored) {
}
}
}
private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int oneByte) {
try {
out.write(oneByte);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void write(byte[] buffer, int offset, int length) {
try {
out.write(buffer, offset, length);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void close() {
try {
out.close();
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void flush() {
try {
out.flush();
} catch (IOException e) {
hasErrors = true;
}
}
}
}
private final class Entry {
private final String key;
/**
* Lengths of this entry's files.
*/
private final long[] lengths;
/**
* True if this entry has ever been published.
*/
private boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
*/
private Editor currentEditor;
/**
* The sequence number of the most recently committed edit to this entry.
*/
private long sequenceNumber;
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
}
public String getLengths() {
StringBuilder result = new StringBuilder();
for (long size : lengths) {
result.append(' ').append(size);
}
return result.toString();
}
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
}
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
}
}
@@ -0,0 +1,522 @@
/*
* 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.beerbelly;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SimpleDiskCache {
private static final String TAG = SimpleDiskCache.class.getSimpleName();
/**
* Disk cache is free
*/
private static final int STATE_DISK_CACHE_NONE = 0;
/**
* Disk cache is flushing or clearing
*/
private static final int STATE_DISK_CACHE_IN_USE = 1;
/**
* Disk cache is reading or writing
*/
private static final int STATE_DISK_CACHE_BUSY = 2;
private final File mCacheDir;
private final int mSize;
@NonNull
private final Map<String, CounterLock> mLockMap = new HashMap<>();
@NonNull
private final LockPool mLockPool = new LockPool();
private int mDiskCacheState;
@Nullable
private DiskLruCache mDiskLruCache;
public SimpleDiskCache(File cacheDir, int size) {
mCacheDir = cacheDir;
mSize = size;
try {
init();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
private static String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
/**
* http://stackoverflow.com/questions/332079
*
* @param bytes The bytes to convert.
* @return A {@link String} converted from the bytes of a hashable key used
* to store a filename on the disk, to hex digits.
*/
private static String bytesToHexString(final byte[] bytes) {
final StringBuilder builder = new StringBuilder();
for (final byte b : bytes) {
final String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
public synchronized boolean isValid() {
return mDiskLruCache != null;
}
private synchronized void init() throws IOException {
if (!isValid()) {
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
}
}
public synchronized long size() {
if (null != mDiskLruCache) {
return mDiskLruCache.size();
} else {
return -1L;
}
}
public synchronized void flush() {
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
if (null != mDiskLruCache) {
try {
mDiskLruCache.flush();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache flush", e);
}
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
public synchronized boolean clear() {
boolean result = true;
if (null == mDiskLruCache) {
return false;
}
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
try {
mDiskLruCache.delete();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache clearCache", e);
}
mDiskLruCache = null;
try {
init();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache init", e);
result = false;
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
return result;
}
private synchronized CounterLock obtainLock(String key) {
// Wait for clear over
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
try {
wait();
} catch (InterruptedException e) {
// Empty
}
}
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
// Get lock from key
CounterLock lock;
lock = mLockMap.get(key);
if (lock == null) {
lock = mLockPool.get();
mLockMap.put(key, lock);
}
lock.obtain();
return lock;
}
private synchronized void releaseLock(String key, CounterLock lock) {
lock.release();
if (lock.isFree()) {
mLockMap.remove(key);
mLockPool.push(lock);
}
if (mLockMap.isEmpty()) {
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
}
public boolean contain(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.readLock().lock();
boolean result = mDiskLruCache.contain(diskKey);
lock.readLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean remove(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result;
try {
result = mDiskLruCache.remove(diskKey);
} catch (IOException e) {
e.printStackTrace();
result = false;
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean put(@NonNull String key, @NonNull InputStream is) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result = false;
if (isValid()) {
result = putToDisk(diskKey, is);
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
private boolean putToDisk(String key, InputStream is) {
DiskLruCache.Editor editor = null;
OutputStream os = null;
boolean completeEdit = false;
try {
//noinspection ConstantConditions
editor = mDiskLruCache.edit(key);
if (editor == null) {
// The editor is in progress
return false;
}
os = editor.newOutputStream(0);
if (os != null) {
Util.copy(is, os);
completeEdit = true;
editor.commit();
return true;
} else {
// Can't get OutputStream
completeEdit = true;
editor.abort();
return false;
}
} catch (IOException e) {
Util.closeQuietly(os);
try {
if (!completeEdit && editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
return false;
}
}
/**
* @param key the key of the target
* @return the InputStreamPipe, <code>null</code> for missing
*/
@Nullable
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
if (contain(key)) {
String diskKey = hashKeyForDisk(key);
return new CacheInputStreamPipe(diskKey);
} else {
return null;
}
}
@NonNull
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
return new CacheOutputStreamPipe(diskKey);
}
private class CacheInputStreamPipe implements InputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Snapshot mCurrentSnapshot;
private CacheInputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.readLock().lock();
}
}
@Override
public void release() {
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.readLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
InputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Snapshot snapshot;
snapshot = mDiskLruCache.get(mKey);
if (snapshot == null) {
throw new IOException("Miss the key " + mKey);
}
mCurrentSnapshot = snapshot;
return snapshot.getInputStream(0);
}
@Override
public void close() {
Util.closeQuietly(mCurrentSnapshot);
mCurrentSnapshot = null;
}
}
private class CacheOutputStreamPipe implements OutputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Editor mCurrentEditor;
private CacheOutputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.writeLock().lock();
}
}
@Override
public void release() {
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.writeLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
OutputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
if (editor == null) {
throw new IOException("Miss the key " + mKey);
}
OutputStream os;
try {
os = editor.newOutputStream(0);
mCurrentEditor = editor;
return os;
} catch (IOException e) {
// Get trouble
try {
editor.abort();
} catch (IOException ex) {
// Ignore
}
throw e;
}
}
@Override
public void close() {
if (mCurrentEditor != null) {
try {
mCurrentEditor.commit();
} catch (IOException e) {
// Empty
}
mCurrentEditor = null;
}
}
}
private class CounterLock extends ReentrantReadWriteLock {
private int mCount;
public boolean isFree() {
return mCount == 0;
}
public void release() {
if (--mCount < 0) {
throw new IllegalStateException("Release time is more than occupy time");
}
}
public void obtain() {
++mCount;
}
}
private class LockPool {
private static final int MAX_SIZE = 10;
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
private final int mMaxSize = MAX_SIZE;
private int mSize;
public void push(CounterLock lock) {
if (lock != null && mSize < mMaxSize) {
mArray[mSize++] = lock;
}
}
public CounterLock get() {
if (mSize > 0) {
CounterLock lock = mArray[--mSize];
mArray[mSize] = null;
return lock;
} else {
return new CounterLock();
}
}
}
}
@@ -0,0 +1,198 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.beerbelly;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
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;
/**
* Buffers input from an {@link InputStream} for reading lines.
*
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
* line at end of input is invalid and will be ignored, the caller may use {@code
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
*
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
*/
class StrictLineReader implements Closeable {
private static final byte CR = (byte) '\r';
private static final byte LF = (byte) '\n';
private final InputStream in;
private final Charset charset;
/*
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
*/
private byte[] buf;
private int pos;
private int end;
/**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
public StrictLineReader(InputStream in, Charset charset) {
this(in, 8192, charset);
}
/**
* Constructs a new {@code LineReader} with the specified capacity and charset.
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
* or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
throw new NullPointerException();
}
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
this.in = in;
this.charset = charset;
buf = new byte[capacity];
}
/**
* Closes the reader by closing the underlying {@code InputStream} and
* marking this reader as closed.
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
buf = null;
in.close();
}
}
}
/**
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
* this end of line marker is not included in the result.
*
* @return the next line from the input.
* @throws IOException for underlying {@code InputStream} errors.
* @throws EOFException for the end of source stream.
*/
public String readLine() throws IOException {
synchronized (in) {
if (buf == null) {
throw new IOException("LineReader is closed");
}
// Read more data if we are at the end of the buffered data.
// Though it's an error to read after an exception, we will let {@code fillBuf()}
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
if (pos >= end) {
fillBuf();
}
// Try to find LF in the buffered data and return the line if successful.
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());
pos = i + 1;
return res;
}
}
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@NonNull
@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.
}
}
};
while (true) {
out.write(buf, pos, end - pos);
// Mark unterminated line in case fillBuf throws EOFException or IOException.
end = -1;
fillBuf();
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
if (i != pos) {
out.write(buf, pos, i - pos);
}
pos = i + 1;
return out.toString();
}
}
}
}
}
public boolean hasUnterminatedLine() {
return end == -1;
}
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
if (result == -1) {
throw new EOFException();
}
pos = 0;
end = result;
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.beerbelly;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Junk drawer of utility methods.
*/
final class Util {
static final Charset US_ASCII = StandardCharsets.US_ASCII;
static final Charset UTF_8 = StandardCharsets.UTF_8;
private static final int EOF = -1;
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private Util() {
}
static String readFully(Reader reader) throws IOException {
try (reader) {
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
}
}
/**
* Deletes the contents of {@code dir}. Throws an IOException if any file
* could not be deleted, or if {@code dir} is not a readable directory.
*/
static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
static void closeQuietly(/*Auto*/Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
*
* @param input the InputStream
* @param output the OutputStream
* @return the number of bytes copied
*/
public static long copy(InputStream input, OutputStream output)
throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
long count = 0;
int n;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
}
@@ -51,7 +51,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 +72,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 +83,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;
}
@@ -219,7 +181,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 +223,6 @@ public final class ConacoTask<V> {
return this;
}
public Builder<T> setDataContainer(DataContainer dataContainer) {
mDataContainer = dataContainer;
return this;
}
boolean isUseMemoryCache() {
return mUseMemoryCache;
}
@@ -332,7 +288,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 +308,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 +394,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 +420,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 +437,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();
}
@@ -1,131 +0,0 @@
/*
* Copyright 2017 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.database;
/*
* Created by Hippo on 2017/9/3.
*/
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MSQLiteBuilder {
public static final String COLUMN_ID = "_id";
private static final Map<Class, String> JAVA_TYPE_TO_SQLITE_TYPE = new HashMap<>();
static {
JAVA_TYPE_TO_SQLITE_TYPE.put(boolean.class, "INTEGER NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(byte.class, "INTEGER NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(short.class, "INTEGER NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(int.class, "INTEGER NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(long.class, "INTEGER NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(float.class, "REAL NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(double.class, "REAL NOT NULL DEFAULT 0");
JAVA_TYPE_TO_SQLITE_TYPE.put(String.class, "TEXT");
}
private final SparseArray<List<String>> statementsMap = new SparseArray<>();
private int version = 0;
private List<String> statements;
private static String javaTypeToSQLiteType(Class clazz) {
String type = JAVA_TYPE_TO_SQLITE_TYPE.get(clazz);
if (type == null) {
throw new IllegalStateException("Unknown type: " + clazz);
}
return type;
}
/**
* Bump database version.
*/
public MSQLiteBuilder version(int version) {
if (version <= this.version) {
throw new IllegalStateException("New version must be bigger than current version. "
+ "current version: " + this.version + ", new version: " + version + ".");
}
this.version = version;
this.statements = new ArrayList<>();
this.statementsMap.put(version, this.statements);
return this;
}
/**
* Creates a table with int {@link #COLUMN_ID} primary key.
*/
public MSQLiteBuilder createTable(String table) {
return createTable(table, COLUMN_ID, int.class);
}
/**
* Creates a table.
*/
public MSQLiteBuilder createTable(String table, String column, Class clazz) {
return statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);");
}
/**
* Drops a table.
*/
public MSQLiteBuilder dropTable(String table) {
return statement("DROP TABLE " + table + ";");
}
/**
* Inserts a column to the table.
*/
public MSQLiteBuilder insertColumn(String table, String column, Class clazz) {
return statement("ALTER TABLE " + table + " ADD COLUMN " + column + " " + javaTypeToSQLiteType(clazz) + ";");
}
/**
* Add a statement.
*/
public MSQLiteBuilder statement(String statement) {
if (version == 0 || statements == null) {
throw new IllegalStateException("Call version() first!");
}
statements.add(statement);
return this;
}
/**
* Build a SQLiteOpenHelper from it.
*/
public SQLiteOpenHelper build(Context context, String name, int version) {
return new MSQLiteOpenHelper(context, name, version, this);
}
List<String> getStatements(int oldVersion, int newVersion) {
List<String> result = new ArrayList<>();
for (int i = oldVersion + 1; i <= newVersion; ++i) {
List<String> list = statementsMap.get(i);
if (list != null) {
result.addAll(list);
}
}
return result;
}
}
@@ -0,0 +1,124 @@
/*
* Copyright 2017 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.database
import android.content.Context
import android.database.sqlite.SQLiteOpenHelper
import android.util.SparseArray
class MSQLiteBuilder {
companion object {
const val COLUMN_ID = "_id"
private val JAVA_TYPE_TO_SQLITE_TYPE: MutableMap<Class<*>?, String> = HashMap()
private fun javaTypeToSQLiteType(clazz: Class<*>?): String {
return JAVA_TYPE_TO_SQLITE_TYPE[clazz]
?: throw IllegalStateException("Unknown type: $clazz")
}
init {
JAVA_TYPE_TO_SQLITE_TYPE[Boolean::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Byte::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Short::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Int::class.javaPrimitiveType] = "INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Long::class.javaPrimitiveType] =
"INTEGER NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Float::class.javaPrimitiveType] =
"REAL NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[Double::class.javaPrimitiveType] = "REAL NOT NULL DEFAULT 0"
JAVA_TYPE_TO_SQLITE_TYPE[String::class.java] = "TEXT"
}
}
private val statementsMap = SparseArray<List<String>?>()
private var version = 0
private var statements: MutableList<String>? = null
/**
* Bump database version.
*/
fun version(version: Int): MSQLiteBuilder {
check(version > this.version) {
("New version must be bigger than current version. "
+ "current version: " + this.version + ", new version: " + version + ".")
}
this.version = version
statements = ArrayList()
statementsMap.put(version, statements)
return this
}
/**
* Creates a table.
*/
/**
* Creates a table with int [.COLUMN_ID] primary key.
*/
@JvmOverloads
fun createTable(
table: String,
column: String = COLUMN_ID,
clazz: Class<*>? = Int::class.javaPrimitiveType
): MSQLiteBuilder {
return statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);")
}
/**
* Drops a table.
*/
fun dropTable(table: String): MSQLiteBuilder {
return statement("DROP TABLE $table;")
}
/**
* Inserts a column to the table.
*/
fun insertColumn(table: String, column: String, clazz: Class<*>?): MSQLiteBuilder {
return statement(
"ALTER TABLE $table ADD COLUMN $column " + javaTypeToSQLiteType(
clazz
) + ";"
)
}
/**
* Add a statement.
*/
fun statement(statement: String): MSQLiteBuilder {
check(!(version == 0 || statements == null)) { "Call version() first!" }
statements!!.add(statement)
return this
}
/**
* Build a SQLiteOpenHelper from it.
*/
fun build(context: Context?, name: String?, version: Int): SQLiteOpenHelper {
return MSQLiteOpenHelper(context, name, version, this)
}
fun getStatements(oldVersion: Int, newVersion: Int): List<String> {
val result: MutableList<String> = ArrayList()
for (i in oldVersion + 1..newVersion) {
val list = statementsMap[i]
if (list != null) {
result.addAll(list)
}
}
return result
}
}
@@ -1,57 +0,0 @@
/*
* Copyright 2017 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.database;
/*
* Created by Hippo on 2017/9/4.
*/
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.hippo.util.SqlUtils;
class MSQLiteOpenHelper extends SQLiteOpenHelper {
private final int version;
private final MSQLiteBuilder builder;
public MSQLiteOpenHelper(Context context, String name, int version, MSQLiteBuilder builder) {
super(context, name, null, version);
this.version = version;
this.builder = builder;
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, version);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for (String command : builder.getStatements(oldVersion, newVersion)) {
db.execSQL(command);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
SqlUtils.dropAllTable(db);
onCreate(db);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2017 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.database
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.hippo.util.SqlUtils
internal class MSQLiteOpenHelper(
context: Context?,
name: String?,
private val version: Int,
private val builder: MSQLiteBuilder
) : SQLiteOpenHelper(context, name, null, version) {
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, version)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
for (command in builder.getStatements(oldVersion, newVersion)) {
db.execSQL(command)
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
SqlUtils.dropAllTable(db)
onCreate(db)
}
}
@@ -1,271 +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.easyrecyclerview;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
// Add overlap
/**
* Only work for {@link androidx.recyclerview.widget.LinearLayoutManager}.<br>
* Show divider between item, just like
* {@link android.widget.ListView#setDivider(android.graphics.drawable.Drawable)}
*/
public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL = LinearLayoutManager.VERTICAL;
private final Rect mRect;
private final Paint mPaint;
private boolean mShowFirstDivider = false;
private boolean mShowLastDivider = false;
private int mOrientation;
private int mThickness;
private int mPaddingStart = 0;
private int mPaddingEnd = 0;
private boolean mOverlap = false;
private ShowDividerHelper mShowDividerHelper;
public LinearDividerItemDecoration(int orientation, int color, int thickness) {
mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
setOrientation(orientation);
setColor(color);
setThickness(thickness);
}
public void setShowDividerHelper(ShowDividerHelper showDividerHelper) {
mShowDividerHelper = showDividerHelper;
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
public void setColor(int color) {
mPaint.setColor(color);
}
public void setThickness(int thickness) {
mThickness = thickness;
}
public void setShowFirstDivider(boolean showFirstDivider) {
mShowFirstDivider = showFirstDivider;
}
public void setShowLastDivider(boolean showLastDivider) {
mShowLastDivider = showLastDivider;
}
public void setPadding(int padding) {
setPaddingStart(padding);
setPaddingEnd(padding);
}
public void setPaddingStart(int paddingStart) {
mPaddingStart = paddingStart;
}
public void setPaddingEnd(int paddingEnd) {
mPaddingEnd = paddingEnd;
}
public void setOverlap(boolean overlap) {
mOverlap = overlap;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
if (parent.getAdapter() == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOverlap) {
outRect.set(0, 0, 0, 0);
return;
}
final int position = parent.getChildLayoutPosition(view);
final int itemCount = parent.getAdapter().getItemCount();
if (mShowDividerHelper != null) {
if (mOrientation == VERTICAL) {
if (position == 0 && mShowDividerHelper.showDivider(0)) {
outRect.top = mThickness;
}
if (mShowDividerHelper.showDivider(position + 1)) {
outRect.bottom = mThickness;
}
} else {
if (position == 0 && mShowDividerHelper.showDivider(0)) {
outRect.left = mThickness;
}
if (mShowDividerHelper.showDivider(position + 1)) {
outRect.right = mThickness;
}
}
} else {
if (mOrientation == VERTICAL) {
if (position == 0 && mShowFirstDivider) {
outRect.top = mThickness;
}
outRect.bottom = mThickness;
if (position == itemCount - 1 && !mShowLastDivider) {
outRect.bottom = 0;
}
} else {
if (position == 0 && mShowFirstDivider) {
outRect.left = mThickness;
}
outRect.right = mThickness;
if (position == itemCount - 1 && !mShowLastDivider) {
outRect.right = 0;
}
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent,
RecyclerView.State state) {
RecyclerView.Adapter adapter = parent.getAdapter();
if (adapter == null) {
return;
}
int itemCount = adapter.getItemCount();
boolean overlap = mOverlap;
if (mOrientation == VERTICAL) {
final boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL;
int mPaddingLeft;
int mPaddingRight;
if (isRtl) {
mPaddingLeft = mPaddingEnd;
mPaddingRight = mPaddingStart;
} else {
mPaddingLeft = mPaddingStart;
mPaddingRight = mPaddingEnd;
}
final int left = parent.getPaddingLeft() + mPaddingLeft;
final int right = parent.getWidth() - parent.getPaddingRight() - mPaddingRight;
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
final int position = parent.getChildLayoutPosition(child);
boolean show;
if (mShowDividerHelper != null) {
show = mShowDividerHelper.showDivider(position + 1);
} else {
show = position != itemCount - 1 || mShowLastDivider;
}
if (show) {
int top = child.getBottom() + lp.bottomMargin;
if (overlap) {
top -= mThickness;
}
final int bottom = top + mThickness;
mRect.set(left, top, right, bottom);
c.drawRect(mRect, mPaint);
}
if (position == 0) {
if (mShowDividerHelper != null) {
show = mShowDividerHelper.showDivider(0);
} else {
show = mShowFirstDivider;
}
if (show) {
int bottom = child.getTop() + lp.topMargin;
if (overlap) {
bottom += mThickness;
}
final int top = bottom - mThickness;
mRect.set(left, top, right, bottom);
c.drawRect(mRect, mPaint);
}
}
}
} else {
final int top = parent.getPaddingTop() + mPaddingStart;
final int bottom = parent.getHeight() - parent.getPaddingBottom() - mPaddingEnd;
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
final int position = parent.getChildLayoutPosition(child);
boolean show;
if (mShowDividerHelper != null) {
show = mShowDividerHelper.showDivider(position + 1);
} else {
show = position != itemCount - 1 || mShowLastDivider;
}
if (show) {
int left = child.getRight() + lp.rightMargin;
if (overlap) {
left -= mThickness;
}
final int right = left + mThickness;
mRect.set(left, top, right, bottom);
c.drawRect(mRect, mPaint);
}
if (position == 0) {
if (mShowDividerHelper != null) {
show = mShowDividerHelper.showDivider(0);
} else {
show = mShowFirstDivider;
}
if (show) {
int right = child.getLeft() + lp.leftMargin;
if (overlap) {
right += mThickness;
}
final int left = right - mThickness;
mRect.set(left, top, right, bottom);
c.drawRect(mRect, mPaint);
}
}
}
}
}
public interface ShowDividerHelper {
boolean showDivider(int index);
}
}
@@ -27,6 +27,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Debug;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.View;
@@ -35,7 +36,12 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.collection.LruCache;
import androidx.core.os.LocaleListCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.Native;
@@ -58,7 +64,6 @@ import com.hippo.image.Image;
import com.hippo.image.ImageBitmap;
import com.hippo.network.StatusCodeException;
import com.hippo.scene.SceneApplication;
import com.hippo.text.Html;
import com.hippo.unifile.UniFile;
import com.hippo.util.BitmapUtils;
import com.hippo.util.ClipboardUtil;
@@ -91,8 +96,6 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import rikka.material.app.DayNightDelegate;
import rikka.material.app.LocaleDelegate;
public class EhApplication extends SceneApplication {
@@ -104,6 +107,8 @@ public class EhApplication extends SceneApplication {
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
private static final long DEBUG_PRINT_INTERVAL = 3000L;
public static boolean locked = false;
private static EhApplication instance;
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
@@ -300,6 +305,7 @@ public class EhApplication extends SceneApplication {
@SuppressLint("StaticFieldLeak")
@Override
public void onCreate() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
instance = this;
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
@@ -325,7 +331,6 @@ public class EhApplication extends SceneApplication {
StatusCodeException.initialize(this);
Settings.initialize(this);
ReadableTime.initialize(this);
Html.initialize(this);
AppConfig.initialize(this);
SpiderDen.initialize(this);
EhDB.initialize(this);
@@ -336,9 +341,8 @@ public class EhApplication extends SceneApplication {
EhDB.mergeOldDB(this);
}
LocaleDelegate.setDefaultLocale(Settings.getLocale());
DayNightDelegate.setApplicationContext(this);
DayNightDelegate.setDefaultNightMode(Settings.getTheme());
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
// Do io tasks in new thread
//noinspection deprecation
@@ -582,4 +586,11 @@ public class EhApplication extends SceneApplication {
return null;
}
}
static class EhlifecycleObserver implements DefaultLifecycleObserver {
@Override
public void onPause(@NonNull LifecycleOwner owner) {
locked = true;
}
}
}
@@ -41,6 +41,7 @@ import com.hippo.yorozuya.NumberUtils;
import java.io.File;
import java.util.Locale;
import java.util.Set;
public class Settings {
@@ -86,7 +87,7 @@ public class Settings {
****** Advanced
********************/
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
public static final String KEY_SECURITY = "security";
public static final String KEY_SECURITY = "require_unlock";
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";
@@ -99,6 +100,11 @@ public class Settings {
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;
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
private static int LIST_THUMB_SIZE = 40;
public static boolean LIST_THUMB_SIZE_INITED = false;
private static final String KEY_AVATAR = "avatar";
private static final String DEFAULT_AVATAR = null;
private static final String KEY_SHOW_WARNING = "show_warning";
@@ -271,6 +277,11 @@ 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";
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static EhConfig sEhConfig;
@@ -384,6 +395,21 @@ public class Settings {
sSettingsPre.edit().putString(key, value).apply();
}
public static Set<String> getStringSet(String key) {
return sSettingsPre.getStringSet(key, null);
}
public static void putStringToStringSet(String key, String value) {
Set<String> set = getStringSet(key);
if (set == null)
set = Set.of(value);
else if (set.contains(value))
return;
else
set.add(value);
sSettingsPre.edit().putStringSet(key, set).apply();
}
public static int getIntFromStr(String key, int defValue) {
try {
return NumberUtils.parseIntSafely(sSettingsPre.getString(key, Integer.toString(defValue)), defValue);
@@ -457,14 +483,6 @@ public class Settings {
putBoolean(KEY_SELECT_SITE, value);
}
public static boolean getQuickSearchTip() {
return getBoolean(KEY_QUICK_SEARCH_TIP, DEFAULT_QUICK_SEARCH_TIP);
}
public static void putQuickSearchTip(boolean value) {
putBoolean(KEY_QUICK_SEARCH_TIP, value);
}
public static int getTheme() {
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
}
@@ -1036,22 +1054,18 @@ public class Settings {
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
}
public static String getSecurity() {
return getString(KEY_SECURITY, DEFAULT_SECURITY);
public static boolean getSecurity() {
return getBoolean(KEY_SECURITY, false);
}
public static void putSecurity(String value) {
putString(KEY_SECURITY, value);
public static void putSecurity(boolean value) {
putBoolean(KEY_SECURITY, value);
}
public static boolean getEnableFingerprint() {
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
}
public static void putEnableFingerprint(boolean value) {
putBoolean(KEY_ENABLE_FINGERPRINT, value);
}
public static int getReadCacheSize() {
return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE);
}
@@ -1151,4 +1165,22 @@ public class Settings {
public static void putClipboardTextHashCode(int value) {
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value);
}
public static Set<String> getArchivePasswds() {
return getStringSet(KEY_ARCHIVE_PASSWDS);
}
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;
}
}
@@ -1,3 +1,21 @@
/*
* 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;
import android.view.View;
@@ -1,3 +1,21 @@
/*
* 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.client;
import android.util.Log;
@@ -1,3 +1,21 @@
/*
* 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.client;
import android.annotation.SuppressLint;
@@ -44,7 +44,7 @@ public class FavListUrlBuilder implements Parcelable {
}
};
private static final String TAG = FavListUrlBuilder.class.getSimpleName();
private int mIndex;
private String mNext;
private String mKeyword;
private int mFavCat = FAV_CAT_ALL;
@@ -52,7 +52,7 @@ public class FavListUrlBuilder implements Parcelable {
}
protected FavListUrlBuilder(Parcel in) {
this.mIndex = in.readInt();
this.mNext = in.readString();
this.mKeyword = in.readString();
this.mFavCat = in.readInt();
}
@@ -61,8 +61,8 @@ public class FavListUrlBuilder implements Parcelable {
return favCat >= 0 && favCat <= 9;
}
public void setIndex(int index) {
mIndex = index;
public void setNext(String next) {
mNext = next;
}
public String getKeyword() {
@@ -105,8 +105,8 @@ public class FavListUrlBuilder implements Parcelable {
Log.e(TAG, "Can't URLEncoder.encode " + mKeyword);
}
}
if (mIndex > 0) {
ub.addQuery("page", Integer.toString(mIndex));
if (mNext != null && !mNext.isEmpty()) {
ub.addQuery("next", mNext);
}
return ub.build();
}
@@ -118,7 +118,7 @@ public class FavListUrlBuilder implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mIndex);
dest.writeString(this.mNext);
dest.writeString(this.mKeyword);
dest.writeInt(this.mFavCat);
}
@@ -64,7 +64,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
@Mode
private int mMode = MODE_NORMAL;
private int mPageIndex = 0;
private int mNextGid = 0;
private int mPrevGid = 0;
private int mCategory = EhUtils.NONE;
private String mKeyword = null;
@@ -86,7 +87,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
@SuppressWarnings("WrongConstant")
protected ListUrlBuilder(Parcel in) {
this.mMode = in.readInt();
this.mPageIndex = in.readInt();
this.mNextGid = in.readInt();
this.mPrevGid = in.readInt();
this.mCategory = in.readInt();
this.mKeyword = in.readString();
this.mAdvanceSearch = in.readInt();
@@ -105,7 +107,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
*/
public void reset() {
mMode = MODE_NORMAL;
mPageIndex = 0;
mNextGid = 0;
mPrevGid = 0;
mCategory = EhUtils.NONE;
mKeyword = null;
mAdvanceSearch = -1;
@@ -137,12 +140,12 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
mMode = mode;
}
public int getPageIndex() {
return mPageIndex;
public void setNextGid(int nextGid) {
mNextGid = nextGid;
}
public void setPageIndex(int pageIndex) {
mPageIndex = pageIndex;
public void setPrevGid(int prevGid) {
mPrevGid = prevGid;
}
public int getCategory() {
@@ -233,7 +236,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
*/
public void set(ListUrlBuilder lub) {
mMode = lub.mMode;
mPageIndex = lub.mPageIndex;
mNextGid = lub.mNextGid;
mPrevGid = lub.mPrevGid;
mCategory = lub.mCategory;
mKeyword = lub.mKeyword;
mAdvanceSearch = lub.mAdvanceSearch;
@@ -524,9 +528,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
if (mSHash != null) {
ub.addQuery("f_shash", mSHash);
}
// Page index
if (mPageIndex != 0) {
ub.addQuery("page", mPageIndex);
if (mNextGid != 0) {
ub.addQuery("next", mNextGid);
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid);
}
// Advance search
if (mAdvanceSearch != -1) {
@@ -570,8 +576,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid);
}
return sb.toString();
}
@@ -583,8 +589,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid);
}
return sb.toString();
}
@@ -600,8 +606,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append("&p=").append(mPageIndex);
if (mNextGid != 0) {
sb.append("&p=").append(mNextGid);
}
return sb.toString();
}
@@ -615,7 +621,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode);
dest.writeInt(this.mPageIndex);
dest.writeInt(this.mNextGid);
dest.writeInt(this.mPrevGid);
dest.writeInt(this.mCategory);
dest.writeString(this.mKeyword);
dest.writeInt(this.mAdvanceSearch);
@@ -1,3 +1,21 @@
/*
* 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.client.parser;
import com.hippo.util.ExceptionUtils;
@@ -31,8 +31,10 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.List;
import java.util.regex.Pattern;
public class FavoritesParser {
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>")) {
@@ -59,14 +61,25 @@ public class FavoritesParser {
e.printStackTrace();
throw new ParseException("Parse favorites error", body);
}
Result re = new Result();
try {
var d = Jsoup.parse(body);
var snv = d.getElementsByClass("searchnav").first();
assert snv != null;
var href = snv.child(3).child(0).attr("href");
var matcher = PATTERN_NEXT_PAGE.matcher(href);
if (matcher.find()) {
re.nextPage = matcher.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 +88,7 @@ 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 nextPage;
public List<GalleryInfo> galleryInfoList;
}
}
@@ -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_GETINT = Pattern.compile("[^0-9]");
private static final String[][] FAVORITE_SLOT_RGB = new String[][]{
new String[]{"0", "0", "0"},
@@ -297,39 +298,21 @@ public class GalleryListParser {
return gi;
}
private static int getInt(String str) {
var m = PATTERN_GETINT.matcher(str);
return Integer.parseInt(m.replaceAll("").trim());
}
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);
}
}
}
} 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;
//noinspection unchecked
result.galleryInfoList = Collections.EMPTY_LIST;
return result;
} else if (d.getElementsByClass("ptt").isEmpty()) {
result.pages = 1;
} else {
result.pages = Integer.MAX_VALUE;
}
result.founds = Integer.MAX_VALUE;
result.noWatchedTags = body.contains("<p>You do not have any watched tags");
if (body.contains("No hits found</p>")) {
result.founds = 0;
result.galleryInfoList = Collections.EMPTY_LIST;
return result;
}
try {
@@ -364,8 +347,7 @@ public class GalleryListParser {
}
public static class Result {
public int pages;
public int nextPage;
public int founds;
public boolean noWatchedTags;
public List<GalleryInfo> galleryInfoList;
}
@@ -1,3 +1,21 @@
/*
* 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.client.parser;
import com.hippo.ehviewer.client.exception.EhException;
@@ -1,3 +1,21 @@
/*
* 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.client.parser;
import com.hippo.util.ExceptionUtils;
@@ -1,3 +1,21 @@
/*
* 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.client.parser;
import org.json.JSONException;
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
@@ -224,8 +223,6 @@ public class DownloadService extends Service implements DownloadManager.Download
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setColorized(true)
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
.setShowWhen(false)
.setChannelId(CHANNEL_ID);
@@ -254,7 +251,6 @@ public class DownloadService extends Service implements DownloadManager.Download
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
@@ -273,7 +269,6 @@ public class DownloadService extends Service implements DownloadManager.Download
.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)))
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR);
@@ -1,174 +0,0 @@
/*
* Copyright 2019 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.gallery;
import androidx.annotation.NonNull;
import com.hippo.a7zip.ArchiveException;
import com.hippo.a7zip.InArchive;
import com.hippo.a7zip.PropID;
import com.hippo.a7zip.PropType;
import com.hippo.a7zip.SeekableInputStream;
import com.hippo.unifile.UniRandomAccessFile;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
class A7ZipArchive implements Closeable {
private final InArchive archive;
private A7ZipArchive(InArchive archive) {
this.archive = archive;
}
private static boolean isSupportedFilename(String name) {
for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) {
if (name.endsWith(extension)) {
return true;
}
}
return false;
}
static A7ZipArchive create(UniRandomAccessFile file) throws ArchiveException {
SeekableInputStream store = new UniRandomAccessFileInStream(file);
InArchive archive = InArchive.open(store);
if ((archive.getArchivePropertyType(PropID.ENCRYPTED) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.ENCRYPTED))
|| (archive.getArchivePropertyType(PropID.SOLID) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.SOLID))
|| (archive.getArchivePropertyType(PropID.IS_VOLUME) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.IS_VOLUME))) {
throw new ArchiveException("Unsupported archive");
}
return new A7ZipArchive(archive);
}
@Override
public void close() {
archive.close();
}
List<A7ZipArchiveEntry> getArchiveEntries() {
List<A7ZipArchiveEntry> entries = new ArrayList<>();
for (int i = 0, n = archive.getNumberOfEntries(); i < n; i++) {
if (!archive.getEntryBooleanProperty(i, PropID.ENCRYPTED)
&& !archive.getEntryBooleanProperty(i, PropID.IS_DIR)
&& !archive.getEntryBooleanProperty(i, PropID.IS_VOLUME)
&& !archive.getEntryBooleanProperty(i, PropID.SOLID)) {
String path = archive.getEntryPath(i);
if (isSupportedFilename(path.toLowerCase())) {
entries.add(new A7ZipArchiveEntry(archive, i, path));
}
}
}
return entries;
}
static class A7ZipArchiveEntry {
private final InArchive archive;
private final int index;
private final String path;
private A7ZipArchiveEntry(InArchive archive, int index, String path) {
this.archive = archive;
this.index = index;
this.path = path;
}
String getPath() {
return path;
}
void extract(OutputStream os) throws ArchiveException {
archive.extractEntry(index, new OutputStreamSequentialOutStream(os));
}
}
private static class UniRandomAccessFileInStream extends SeekableInputStream {
private final byte[] scratch = new byte[8];
private final UniRandomAccessFile file;
public UniRandomAccessFileInStream(UniRandomAccessFile file) {
this.file = file;
}
@Override
public void seek(long pos) throws IOException {
file.seek(pos);
}
@Override
public long tell() throws IOException {
return file.getFilePointer();
}
@Override
public long size() throws IOException {
return file.length();
}
@Override
public int read() throws IOException {
return (file.read(scratch, 0, 1) != -1) ? scratch[0] & 0xff : -1;
}
@Override
public int read(@NonNull byte[] b) throws IOException {
return file.read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return file.read(b, off, len);
}
@Override
public void close() throws IOException {
file.close();
}
}
private static class OutputStreamSequentialOutStream extends OutputStream {
private final OutputStream stream;
public OutputStreamSequentialOutStream(OutputStream stream) {
this.stream = stream;
}
@Override
public void write(int b) throws IOException {
stream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
stream.write(b, off, len);
}
@Override
public void close() throws IOException {
stream.close();
}
}
}
@@ -18,44 +18,36 @@ package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.a7zip.ArchiveException;
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.unifile.UniRandomAccessFile;
import com.hippo.util.NaturalComparator;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
public class ArchiveGalleryProvider extends GalleryProvider2 {
private static final AtomicInteger sIdGenerator = new AtomicInteger();
private static final Comparator<A7ZipArchive.A7ZipArchiveEntry> naturalComparator = new Comparator<A7ZipArchive.A7ZipArchiveEntry>() {
private final NaturalComparator comparator = new NaturalComparator();
@Override
public int compare(A7ZipArchive.A7ZipArchiveEntry o1, A7ZipArchive.A7ZipArchiveEntry o2) {
return comparator.compare(o1.getPath(), o2.getPath());
}
};
private final UniFile file;
public static Handler showPasswd;
public static String passwd;
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, InputStream> streams = new LinkedHashMap<>();
@@ -66,7 +58,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
private String error;
public ArchiveGalleryProvider(Context context, Uri uri) {
file = UniFile.fromMediaUri(context, uri);
UriArchiveAccessor archiveAccessor1;
try {
archiveAccessor1 = new UriArchiveAccessor(context, uri);
} catch (Exception e) {
archiveAccessor1 = null;
e.printStackTrace();
}
archiveAccessor = archiveAccessor1;
}
@Override
@@ -96,6 +95,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
decodeThread.interrupt();
decodeThread = null;
}
try {
archiveAccessor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
@@ -165,41 +169,64 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
private class ArchiveTask implements Runnable {
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
}
private void notifyError() {
showPasswd.sendEmptyMessage(1);
}
private void notifyDismiss() {
showPasswd.sendEmptyMessage(2);
}
@Override
public void run() {
UniRandomAccessFile uraf = null;
try {
uraf = file.createRandomAccessFile("r");
} catch (IOException e) {
size = archiveAccessor.open();
} catch (Exception e) {
e.printStackTrace();
size = 0;
}
if (uraf == null) {
if (size <= 0) {
size = STATE_ERROR;
error = GetText.getString(R.string.error_reading_failed);
notifyDataChanged();
return;
}
A7ZipArchive archive = null;
try {
archive = A7ZipArchive.create(uraf);
} catch (ArchiveException e) {
e.printStackTrace();
}
if (archive == null) {
size = STATE_ERROR;
error = GetText.getString(R.string.error_invalid_archive);
notifyDataChanged();
return;
}
List<A7ZipArchive.A7ZipArchiveEntry> entries = archive.getArchiveEntries();
Collections.sort(entries, naturalComparator);
// Update size and notify changed
size = entries.size();
notifyDataChanged();
if (archiveAccessor.needPassword()) {
boolean need_request = true;
Set<String> set = Settings.getArchivePasswds();
if (set != null) {
for (String passwd : set) {
if (archiveAccessor.providePassword(passwd)) {
need_request = false;
break;
}
}
}
while (!Thread.currentThread().isInterrupted() && need_request) {
try {
waitPasswd();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
if (!archiveAccessor.providePassword(passwd))
notifyError();
else {
Settings.putPasswdToArchivePasswds(passwd);
notifyDismiss();
break;
}
}
}
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
@@ -217,13 +244,13 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
// Check index valid
if (index < 0 || index >= entries.size()) {
if (index < 0 || index >= size) {
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
continue;
}
Pipe pipe = new Pipe(4 * 1024);
Pipe pipe = new Pipe();
synchronized (streams) {
if (streams.get(index) != null) {
@@ -233,13 +260,8 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
streams.notify();
}
try {
entries.get(index).extract(pipe.getOutputStream());
} catch (ArchiveException e) {
e.printStackTrace();
} finally {
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
}
archiveAccessor.extracttoOutputStream(index, pipe.getOutputFd());
pipe.closeOutputFd();
}
}
}
@@ -282,4 +304,4 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
}
}
}
@@ -16,148 +16,37 @@
package com.hippo.ehviewer.gallery;
import androidx.annotation.NonNull;
import android.os.ParcelFileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
class Pipe {
private final int capacity;
private final byte[] buffer;
private int head = 0;
private int tail = 0;
private boolean full = false;
private boolean inClosed = false;
private boolean outClosed = false;
private final InputStream inputStream = new InputStream() {
@Override
public int read() throws IOException {
synchronized (Pipe.this) {
byte[] bytes = new byte[1];
if (read(bytes, 0, 1) != -1) {
return bytes[0];
} else {
return -1;
}
}
private ParcelFileDescriptor[] fd;
{
try {
fd = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
e.printStackTrace();
}
@Override
public int read(@NonNull byte[] b, int off, int len) throws IOException {
synchronized (Pipe.this) {
for (; ; ) {
if (inClosed) {
throw new IOException("The InputStream is closed");
}
if (len == 0) {
return 0;
}
if (head == tail && !full) {
if (outClosed) {
// No bytes available and the OutputStream is closed. So it's the end.
return -1;
} else {
// Wait for OutputStream write bytes
try {
Pipe.this.wait();
} catch (InterruptedException e) {
throw new IOException("The thread interrupted", e);
}
}
} else {
int read = Math.min(len, (head < tail ? tail : capacity) - head);
System.arraycopy(buffer, head, b, off, read);
head += read;
if (head == capacity) {
head = 0;
}
full = false;
Pipe.this.notifyAll();
return read;
}
}
}
}
@Override
public void close() {
synchronized (Pipe.this) {
inClosed = true;
Pipe.this.notifyAll();
}
}
};
private final OutputStream outputStream = new OutputStream() {
@Override
public void write(int b) throws IOException {
synchronized (Pipe.this) {
byte[] bytes = new byte[]{(byte) b};
write(bytes, 0, 1);
}
}
@Override
public void write(@NonNull byte[] b, int off, int len) throws IOException {
synchronized (Pipe.this) {
while (len != 0) {
if (outClosed) {
throw new IOException("The OutputStream is closed");
}
if (inClosed) {
throw new IOException("The InputStream is closed");
}
if (head == tail && full) {
// The buffer is full, wait for InputStream read bytes
try {
Pipe.this.wait();
} catch (InterruptedException e) {
throw new IOException("The thread interrupted", e);
}
} else {
int write = Math.min(len, (head <= tail ? capacity : head) - tail);
System.arraycopy(b, off, buffer, tail, write);
off += write;
len -= write;
tail += write;
if (tail == capacity) {
tail = 0;
}
if (head == tail) {
full = true;
}
Pipe.this.notifyAll();
}
}
}
}
@Override
public void close() {
synchronized (Pipe.this) {
outClosed = true;
Pipe.this.notifyAll();
}
}
};
Pipe(int capacity) {
this.capacity = capacity;
this.buffer = new byte[capacity];
}
private final InputStream inputStream = new FileInputStream(fd[0].getFileDescriptor());
InputStream getInputStream() {
return inputStream;
}
OutputStream getOutputStream() {
return outputStream;
int getOutputFd() {
return fd[1].getFd();
}
void closeOutputFd() {
try {
fd[1].close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -18,6 +18,7 @@ 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;
@@ -29,7 +30,6 @@ 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.text.Html;
import com.hippo.util.ClipboardUtil;
import java.util.LinkedList;
@@ -92,7 +92,7 @@ public class IdentityCookiePreference extends MessagePreference {
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)));
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));
@@ -1,191 +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.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;
import android.widget.Spinner;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhProxySelector;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.network.InetValidator;
import com.hippo.preference.DialogPreference;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.ViewUtils;
public class ProxyPreference extends DialogPreference implements View.OnClickListener {
private Spinner mType;
private TextInputLayout mIpInputLayout;
private EditText mIp;
private TextInputLayout mPortInputLayout;
private EditText mPort;
public ProxyPreference(Context context) {
super(context);
init();
}
public ProxyPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ProxyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setDialogLayoutResource(R.layout.preference_dialog_proxy);
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort());
}
private String getProxyTypeText(Context context, int type) {
String[] array = context.getResources().getStringArray(R.array.proxy_types);
return array[MathUtils.clamp(type, 0, array.length - 1)];
}
private void updateSummary(int type, String ip, int port) {
if ((type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS)
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))) {
type = EhProxySelector.TYPE_SYSTEM;
}
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
Context context = getContext();
setSummary(context.getString(R.string.settings_advanced_proxy_summary_1,
getProxyTypeText(context, type),
ip,
port));
} else {
Context context = getContext();
setSummary(context.getString(R.string.settings_advanced_proxy_summary_2,
getProxyTypeText(context, type)));
}
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
builder.setPositiveButton(android.R.string.ok, null);
}
@Override
@SuppressLint("SetTextI18n")
protected void onDialogCreated(AlertDialog dialog) {
super.onDialogCreated(dialog);
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this);
mType = (Spinner) ViewUtils.$$(dialog, R.id.type);
mIpInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.ip_input_layout);
mIp = (EditText) ViewUtils.$$(dialog, R.id.ip);
mPortInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.port_input_layout);
mPort = (EditText) ViewUtils.$$(dialog, R.id.port);
int type = Settings.getProxyType();
String[] array = getContext().getResources().getStringArray(R.array.proxy_types);
mType.setSelection(MathUtils.clamp(type, 0, array.length));
mIp.setText(Settings.getProxyIp());
String portString;
int port = Settings.getProxyPort();
if (!InetValidator.isValidInetPort(port)) {
portString = null;
} else {
portString = Integer.toString(port);
}
mPort.setText(portString);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
mType = null;
mIpInputLayout = null;
mIp = null;
mPortInputLayout = null;
mPort = null;
}
@Override
public void onClick(View v) {
Dialog dialog = getDialog();
Context context = getContext();
if (null == dialog || null == context || null == mType ||
null == mIpInputLayout || null == mIp ||
null == mPortInputLayout || null == mPort) {
return;
}
int type = mType.getSelectedItemPosition();
String ip = mIp.getText().toString().trim();
if (ip.isEmpty()) {
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
mIpInputLayout.setError(context.getString(R.string.text_is_empty));
return;
}
}
mIpInputLayout.setError(null);
int port;
String portString = mPort.getText().toString().trim();
if (portString.isEmpty()) {
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
mPortInputLayout.setError(context.getString(R.string.text_is_empty));
return;
} else {
port = -1;
}
} else {
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
port = -1;
}
if (!InetValidator.isValidInetPort(port)) {
mPortInputLayout.setError(context.getString(R.string.proxy_invalid_port));
return;
}
}
mPortInputLayout.setError(null);
Settings.putProxyType(type);
Settings.putProxyIp(ip);
Settings.putProxyPort(port);
updateSummary(type, ip, port);
EhApplication.getEhProxySelector(getContext()).updateProxy();
dialog.dismiss();
}
}
@@ -0,0 +1,167 @@
/*
* 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.content.DialogInterface
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
import android.widget.AutoCompleteTextView
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhProxySelector
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.network.InetValidator
import com.hippo.preference.DialogPreference
import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.ViewUtils
class ProxyPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : DialogPreference(context, attrs), View.OnClickListener {
private var mType: TextInputLayout? = null
private var mIpInputLayout: TextInputLayout? = null
private var mIp: EditText? = null
private var mPortInputLayout: TextInputLayout? = null
private var mPort: EditText? = null
private val mArray: Array<String>
init {
mArray = context.resources.getStringArray(R.array.proxy_types)
dialogLayoutResource = R.layout.preference_dialog_proxy
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort())
}
private fun getProxyTypeText(type: Int): String {
return mArray[MathUtils.clamp(type, 0, mArray.size - 1)]
}
private fun updateSummary(type: Int, ip: String?, port: Int) {
var type1 = type
if ((type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS)
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))
) {
type1 = EhProxySelector.TYPE_SYSTEM
}
summary = if (type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS) {
val context = context
context.getString(
R.string.settings_advanced_proxy_summary_1,
getProxyTypeText(type1),
ip,
port
)
} else {
val context = context
context.getString(
R.string.settings_advanced_proxy_summary_2,
getProxyTypeText(type1)
)
}
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setPositiveButton(android.R.string.ok, null)
}
@SuppressLint("SetTextI18n")
override fun onDialogCreated(dialog: AlertDialog) {
super.onDialogCreated(dialog)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this)
mType = ViewUtils.`$$`(dialog, R.id.type) as TextInputLayout
val array = context.resources.getStringArray(R.array.proxy_types)
mIpInputLayout = ViewUtils.`$$`(dialog, R.id.ip_input_layout) as TextInputLayout
mIp = ViewUtils.`$$`(dialog, R.id.ip) as EditText
mPortInputLayout = ViewUtils.`$$`(dialog, R.id.port_input_layout) as TextInputLayout
mPort = ViewUtils.`$$`(dialog, R.id.port) as EditText
val type = Settings.getProxyType()
(mType!!.editText as AutoCompleteTextView).setText(
array[MathUtils.clamp(
type,
0,
array.size
)], false
)
mIp!!.setText(Settings.getProxyIp())
val portString: String?
val port = Settings.getProxyPort()
portString = if (!InetValidator.isValidInetPort(port)) {
null
} else {
port.toString()
}
mPort!!.setText(portString)
}
override fun onDialogClosed(positiveResult: Boolean) {
super.onDialogClosed(positiveResult)
mType = null
mIpInputLayout = null
mIp = null
mPortInputLayout = null
mPort = null
}
override fun onClick(v: View) {
val dialog = dialog
val context: Context = context
if (null == dialog || null == mType || null == mIpInputLayout || null == mIp || null == mPortInputLayout || null == mPort) {
return
}
val type = mArray.indexOf(mType!!.editText!!.text.toString())
val ip = mIp!!.text.toString().trim { it <= ' ' }
if (ip.isEmpty()) {
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
mIpInputLayout!!.error = context.getString(R.string.text_is_empty)
return
}
}
mIpInputLayout!!.error = null
val port: Int
val portString = mPort!!.text.toString().trim { it <= ' ' }
if (portString.isEmpty()) {
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
mPortInputLayout!!.error = context.getString(R.string.text_is_empty)
return
} else {
port = -1
}
} else {
port = try {
portString.toInt()
} catch (e: NumberFormatException) {
-1
}
if (!InetValidator.isValidInetPort(port)) {
mPortInputLayout!!.error = context.getString(R.string.proxy_invalid_port)
return
}
}
mPortInputLayout!!.error = null
Settings.putProxyType(type)
Settings.putProxyIp(ip)
Settings.putProxyPort(port)
updateSummary(type, ip, port)
EhApplication.getEhProxySelector(getContext()).updateProxy()
dialog.dismiss()
}
}
@@ -1,58 +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.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.AttributeSet;
import androidx.preference.Preference;
import com.hippo.ehviewer.R;
public class VersionPreference extends Preference {
public VersionPreference(Context context) {
super(context);
init(context);
}
public VersionPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public VersionPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setTitle(R.string.settings_about_version);
String version;
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
version = pInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
version = context.getString(R.string.error_unknown);
}
setSummary(version);
}
}
@@ -0,0 +1,68 @@
/*
* 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.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.hippo.Native
import com.hippo.ehviewer.R
import com.hippo.preference.MessagePreference
class VersionPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MessagePreference(context, attrs) {
init {
setTitle(R.string.settings_about_version)
@Suppress("DEPRECATION") val version: String = try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
context.getString(R.string.error_unknown)
}
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("libjpeg-turbo", Native.getlibjpeg_turboVersion())
.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
}
}
}
@@ -45,6 +45,10 @@ import java.util.List;
public final class CommonOperations {
public static int getPagesForFounds(int founds, int per) {
return (int)Math.ceil((double) founds/per);
}
private static void doAddToFavorites(Activity activity, GalleryInfo galleryInfo,
int slot, EhClient.Callback<Void> listener) {
if (slot == -1) {
@@ -20,23 +20,25 @@ import android.content.Context;
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.NonNull;
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.material.app.MaterialActivity;
import rikka.insets.WindowInsetsHelper;
import rikka.layoutinflater.view.LayoutInflaterFactory;
public abstract class EhActivity extends MaterialActivity {
public abstract class EhActivity extends AppCompatActivity {
private static final String THEME_DEFAULT = "DEFAULT";
private static final String THEME_BLACK = "BLACK";
@@ -55,16 +57,6 @@ public abstract class EhActivity extends MaterialActivity {
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
}
@Override
public void onApplyUserThemeResource(@NonNull Resources.Theme theme, boolean isDecorView) {
theme.applyStyle(getThemeStyleRes(this), true);
}
@Override
public String computeUserThemeKey() {
return getTheme(this);
}
@StyleRes
public int getThemeStyleRes(Context context) {
if (THEME_BLACK.equals(getTheme(context))) {
@@ -73,9 +65,53 @@ public abstract class EhActivity extends MaterialActivity {
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);
}
@@ -95,22 +131,4 @@ public abstract class EhActivity extends MaterialActivity {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
@Override
public void onApplyTranslucentSystemBars() {
super.onApplyTranslucentSystemBars();
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) {
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
window.setNavigationBarContrastEnforced(false);
} else {
window.setNavigationBarColor(Color.TRANSPARENT);
window.setNavigationBarContrastEnforced(true);
}
});
}
}
@@ -37,6 +37,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -49,7 +51,6 @@ import android.webkit.MimeTypeMap;
import android.widget.FrameLayout;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
@@ -58,6 +59,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.view.ViewCompat;
@@ -66,6 +68,8 @@ 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.EditTextDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.BuildConfig;
import com.hippo.ehviewer.R;
@@ -102,10 +106,10 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import rikka.core.res.ConfigurationKt;
import rikka.core.res.ResourcesKt;
import rikka.material.app.DayNightDelegate;
public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener,
GalleryView.Listener {
@@ -238,28 +242,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
} else if (Intent.ACTION_VIEW.equals(mAction)) {
if (mUri != null) {
// Only support zip now
try {
grantUriPermission(BuildConfig.APPLICATION_ID, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (Exception e) {
// Some stupid file manager send us a uri and don't allow us to read it
// java.lang.SecurityException: UID 10671 does not have permission to
// content://com.UCMobile.fileProvider/external_files/BaiduNetdisk/getvoice [user 0]
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show();
}
mGalleryProvider = new ArchiveGalleryProvider(this, mUri);
}
}
} 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;
});
}
private void onInit() {
Intent intent = getIntent();
@@ -302,20 +293,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
protected void attachBaseContext(@NonNull Context newBase) {
switch (Settings.getReadTheme()) {
case 1:
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_YES, false);
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case 2:
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_NO, false);
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
}
super.attachBaseContext(newBase);
}
@Override
public boolean respectDefaultNightMode() {
return Settings.getReadTheme() == 0;
}
@Override
@SuppressWarnings({"WrongConstant"})
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -332,10 +318,19 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
onRestore(savedInstanceState);
}
builder = new EditTextDialogBuilder(this, null, getString(R.string.archive_passwd));
builder.setTitle(getString(R.string.archive_need_passwd));
builder.setPositiveButton(getString(android.R.string.ok), null);
dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
if (mGalleryProvider == null) {
finish();
return;
}
ArchiveGalleryProvider.showPasswd = new ShowPasswdDialogHandler(this);
mGalleryProvider.start();
// Get start page
@@ -357,7 +352,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
.setStartPosition(Settings.getStartPosition())
.setStartPage(startPage)
.setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground))
.setEdgeColor(ResourcesKt.resolveColor(getTheme(), R.attr.colorEdgeEffect) & 0xffffff | 0x33000000)
.setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0)
.setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0)
.setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height))
@@ -400,17 +394,22 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL:
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300;
if (scroll < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else {
} 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 {
} 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);
}
}
}
@@ -483,7 +482,16 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main);
mainLayout.addView(new GalleryGuideView(this));
}
}
} 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 onDestroy() {
@@ -1051,15 +1059,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
private final Spinner mScaleMode;
private final Spinner mStartPosition;
private final Spinner mReadTheme;
private final Switch mKeepScreenOn;
private final Switch mShowClock;
private final Switch mShowProgress;
private final Switch mShowBattery;
private final Switch mShowPageInterval;
private final Switch mVolumePage;
private final Switch mReverseVolumePage;
private final Switch mReadingFullscreen;
private final Switch mCustomScreenLightness;
private final MaterialSwitch mKeepScreenOn;
private final MaterialSwitch mShowClock;
private final MaterialSwitch mShowProgress;
private final MaterialSwitch mShowBattery;
private final MaterialSwitch mShowPageInterval;
private final MaterialSwitch mVolumePage;
private final MaterialSwitch mReverseVolumePage;
private final MaterialSwitch mReadingFullscreen;
private final MaterialSwitch mCustomScreenLightness;
private final SeekBar mScreenLightness;
@SuppressLint("InflateParams")
@@ -1303,7 +1311,69 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
}
private EditTextDialogBuilder builder;
private boolean dialogShown = false;
private AlertDialog dialog;
private void showPasswdDialog() {
if (!dialogShown) {
dialogShown = true;
dialog.show();
if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
GalleryActivity.this.onProvidePasswd();
});
}
dialog.setOnCancelListener(v -> finish());
}
}
private void onProvidePasswd() {
String passwd = builder.getText();
if (passwd.isEmpty())
builder.setError(getString(R.string.passwd_cannot_be_empty));
else {
ArchiveGalleryProvider.passwd = passwd;
ArchiveGalleryProvider.pv.v();
}
}
private void onPasswdWrong() {
builder.setError(getString(R.string.passwd_wrong));
}
private void onPasswdCorrect() {
dialog.dismiss();
}
private static class ShowPasswdDialogHandler extends Handler {
//弱引用持有HandlerActivity , GC 回收时会被回收掉
private final WeakReference<GalleryActivity> weakReference;
public ShowPasswdDialogHandler(GalleryActivity activity) {
this.weakReference = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
GalleryActivity activity = weakReference.get();
super.handleMessage(msg);
if (null != activity) {
switch (msg.what) {
case 0:
activity.showPasswdDialog();
break;
case 1:
activity.onPasswdWrong();
break;
case 2:
activity.onPasswdCorrect();
break;
}
}
}
}
}
@@ -28,7 +28,6 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
@@ -48,6 +47,7 @@ import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhTagDatabase;
@@ -143,7 +143,7 @@ public final class MainActivity extends StageActivity
@NonNull
@Override
protected Announcer getLaunchAnnouncer() {
if (!TextUtils.isEmpty(Settings.getSecurity())) {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
return new Announcer(SecurityScene.class);
} else if (Settings.getShowWarning()) {
return new Announcer(WarningScene.class);
@@ -161,7 +161,7 @@ public final class MainActivity extends StageActivity
// Sometimes scene can't show directly
private Announcer processAnnouncer(Announcer announcer) {
if (0 == getSceneCount()) {
if (!TextUtils.isEmpty(Settings.getSecurity())) {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
Bundle newArgs = new Bundle();
newArgs.putString(SecurityScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
newArgs.putBundle(SecurityScene.KEY_TARGET_ARGS, announcer.getArgs());
@@ -307,7 +307,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);
@@ -361,9 +360,15 @@ public final class MainActivity extends StageActivity
.setTitle(R.string.app_link_not_verified_title)
.setMessage(R.string.app_link_not_verified_message)
.setPositiveButton(R.string.open_settings, (dialogInterface, i) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + getPackageName()));
startActivity(intent);
try {
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Throwable t) {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
})
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.dont_show_again, (dialogInterface, i) -> Settings.putAppLinkVerifyTip(true))
@@ -421,6 +426,9 @@ public final class MainActivity extends StageActivity
@Override
protected void onResume() {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this) && EhApplication.locked) {
startScene(new Announcer(SecurityScene.class), true);
}
super.onResume();
setNavCheckedItem(mNavCheckedItem);
checkClipboardUrl();
@@ -1,66 +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 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()
.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,64 @@
/*
* 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 androidx.annotation.StringRes
import androidx.core.view.WindowCompat
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
class SettingsActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preference)
setSupportActionBar(findViewById(R.id.toolbar))
val bar = supportActionBar
bar?.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) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
}
@@ -17,12 +17,12 @@
package com.hippo.ehviewer.ui.fragment;
import android.os.Bundle;
import android.text.Html;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.hippo.ehviewer.R;
import com.hippo.text.Html;
public class AboutFragment extends BasePreferenceFragment {
@@ -30,10 +30,10 @@ public class AboutFragment extends BasePreferenceFragment {
private static final String KEY_CHECK_FOR_UPDATES = "check_for_updates";
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.about_settings);
Preference author = findPreference(KEY_AUTHOR);
author.setSummary(Html.fromHtml(getString(R.string.settings_about_author_summary).replace('$', '@')));
author.setSummary(Html.fromHtml(getString(R.string.settings_about_author_summary).replace('$', '@'), Html.FROM_HTML_MODE_LEGACY));
}
@Override
@@ -17,16 +17,20 @@
package com.hippo.ehviewer.ui.fragment;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.os.LocaleListCompat;
import androidx.preference.Preference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -36,6 +40,11 @@ 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;
@@ -50,12 +59,10 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import rikka.material.app.LocaleDelegate;
public class AdvancedFragment extends BasePreferenceFragment {
private static final String KEY_DUMP_LOGCAT = "dump_logcat";
@@ -64,6 +71,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
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(),
@@ -222,7 +230,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
});
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.advanced_settings);
Preference dumpLogcat = findPreference(KEY_DUMP_LOGCAT);
@@ -231,6 +239,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
Preference importData = findPreference(KEY_IMPORT_DATA);
Preference exportData = findPreference(KEY_EXPORT_DATA);
Preference openByDefault = findPreference(KEY_OPEN_BY_DEFAULT);
Preference backupFavorite = findPreference(KEY_BACKUP_FAVORITE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
openByDefault.setVisible(false);
@@ -242,6 +251,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
clearMemoryCache.setOnPreferenceClickListener(this);
importData.setOnPreferenceClickListener(this);
exportData.setOnPreferenceClickListener(this);
backupFavorite.setOnPreferenceClickListener(this);
appLanguage.setOnPreferenceChangeListener(this);
}
@@ -278,22 +288,80 @@ public class AdvancedFragment extends BasePreferenceFragment {
}
return true;
} else if (KEY_OPEN_BY_DEFAULT.equals(key)) {
Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + requireContext().getPackageName()));
startActivity(intent);
try {
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + requireContext().getPackageName()));
startActivity(intent);
} catch (Throwable t) {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + requireContext().getPackageName()));
startActivity(intent);
}
return true;
} else if (KEY_BACKUP_FAVORITE.equals(key)) {
try {
showTip(R.string.backup_favorite_start, BaseScene.LENGTH_SHORT);
backupFavorite();
} catch (Exception e) {
ExceptionUtils.throwIfFatal(e);
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
return true;
}
return false;
}
private void backupFavorite() {
AtomicInteger page = new AtomicInteger(0);
Context context = requireContext();
Activity activity = requireActivity();
EhClient mClient = EhApplication.getEhClient(context);
FavListUrlBuilder favListUrlBuilder = new FavListUrlBuilder();
// favListUrlBuilder.setNext(page.get());
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
request.setCallback(new EhClient.Callback() {
@Override
public void onSuccess(Object result) {
try {
FavoritesParser.Result result1 = (FavoritesParser.Result) result;
EhDB.putLocalFavorites(result1.galleryInfoList);
Log.d("LocalFavorites", "now backup page " + page.get());
if (!result1.galleryInfoList.isEmpty()) {
// favListUrlBuilder.setNext(page.incrementAndGet());
request.setArgs(favListUrlBuilder.build(), Settings.getShowJpnTitle());
mClient.execute(request);
} else {
showTip(R.string.backup_favorite_success, BaseScene.LENGTH_SHORT);
}
} catch (Exception e) {
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
}
@Override
public void onFailure(Exception e) {
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
@Override
public void onCancel() {
showTip(R.string.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 ("system".equals(newValue)) {
LocaleDelegate.setDefaultLocale(LocaleDelegate.getSystemLocale());
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
} else {
LocaleDelegate.setDefaultLocale(Locale.forLanguageTag((String) newValue));
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags((String) newValue));
}
requireActivity().recreate();
return true;
@@ -1,3 +1,21 @@
/*
* 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;
@@ -1,18 +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/>.
*/
package com.hippo.ehviewer.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import androidx.recyclerview.widget.RecyclerView;
import androidx.preference.PreferenceFragmentCompat;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import rikka.recyclerview.RecyclerViewKt;
public class BasePreferenceFragment extends PreferenceFragmentCompat
implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
@@ -38,10 +55,26 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
}
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
String key = preference.getKey();
try {
Fragment classObj = (Fragment) Class.forName(key).newInstance();
requireActivity().getSupportFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.replace(R.id.fragment, classObj)
.addToBackStack(null)
.commitAllowingStateLoss();
} catch (ClassNotFoundException | IllegalAccessException | java.lang.InstantiationException ignored) {
}
return true;
}
private void setTitle(@StringRes int string) {
requireActivity().setTitle(string);
}
@@ -53,11 +86,4 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
public void showTip(CharSequence message, int length) {
((SettingsActivity) requireActivity()).showTip(message, length);
}
@Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
RecyclerViewKt.fixEdgeEffect(recyclerView, false, true);
return recyclerView;
}
}
@@ -59,7 +59,7 @@ public class DownloadFragment extends BasePreferenceFragment {
});
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.download_settings);
Preference mediaScan = findPreference(Settings.KEY_MEDIA_SCAN);
@@ -21,24 +21,24 @@ 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;
import rikka.material.app.DayNightDelegate;
public class EhFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
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 listThumbSize = findPreference(Settings.KEY_LIST_THUMB_SIZE);
Preference detailSize = findPreference(Settings.KEY_DETAIL_SIZE);
Preference thumbSize = findPreference(Settings.KEY_THUMB_SIZE);
Preference showTagTranslations = findPreference(Settings.KEY_SHOW_TAG_TRANSLATIONS);
@@ -47,6 +47,7 @@ public class EhFragment extends BasePreferenceFragment {
theme.setOnPreferenceChangeListener(this);
gallerySite.setOnPreferenceChangeListener(this);
listMode.setOnPreferenceChangeListener(this);
listThumbSize.setOnPreferenceChangeListener(this);
detailSize.setOnPreferenceChangeListener(this);
thumbSize.setOnPreferenceChangeListener(this);
showTagTranslations.setOnPreferenceChangeListener(this);
@@ -62,7 +63,7 @@ public class EhFragment extends BasePreferenceFragment {
public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey();
if (Settings.KEY_THEME.equals(key)) {
DayNightDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
AppCompatDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
requireActivity().recreate();
return true;
} else if (Settings.KEY_GALLERY_SITE.equals(key)) {
@@ -71,6 +72,10 @@ public class EhFragment extends BasePreferenceFragment {
} else if (Settings.KEY_LIST_MODE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
return true;
} else if (Settings.KEY_LIST_THUMB_SIZE.equals(key)) {
Settings.LIST_THUMB_SIZE_INITED = false;
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)) {
@@ -1,465 +0,0 @@
package com.hippo.ehviewer.ui.fragment;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
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.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
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.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhFilter;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.List;
import rikka.core.res.ResourcesKt;
public class FilterFragment extends BaseFragment {
@Nullable
private ViewTransition mViewTransition;
@Nullable
private FilterAdapter mAdapter;
@Nullable
private FilterList mFilterList;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.activity_filter, container, false);
mFilterList = new FilterList();
RecyclerView recyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(recyclerView, tip);
FloatingActionButton fab = view.findViewById(R.id.fab);
Drawable drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
tip.setCompoundDrawables(null, drawable, null, null);
mAdapter = new FilterAdapter();
mAdapter.setHasStableIds(true);
recyclerView.setAdapter(mAdapter);
recyclerView.setClipToPadding(false);
recyclerView.setClipChildren(false);
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(requireActivity().getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(requireActivity(), 1));
decoration.setShowLastDivider(true);
recyclerView.addItemDecoration(decoration);
recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
recyclerView.setHasFixedSize(true);
DefaultItemAnimator defaultItemAnimator = (DefaultItemAnimator) recyclerView.getItemAnimator();
if (defaultItemAnimator != null) {
defaultItemAnimator.setSupportsChangeAnimations(false);
}
fab.setOnClickListener(v -> showAddFilterDialog());
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
updateView(false);
}
private void updateView(boolean animation) {
if (null == mViewTransition) {
return;
}
if (null == mFilterList || 0 == mFilterList.size()) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mViewTransition = null;
mAdapter = null;
mFilterList = null;
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.activity_filter, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_tip) {
showTipDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
private void showTipDialog() {
new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.filter)
.setMessage(R.string.filter_tip)
.setPositiveButton(android.R.string.ok, null)
.show();
}
private void showAddFilterDialog() {
AlertDialog dialog = new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.add_filter)
.setView(R.layout.dialog_add_filter)
.setPositiveButton(R.string.add, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
AddFilterDialogHelper helper = new AddFilterDialogHelper();
helper.setDialog(dialog);
}
private void showDeleteFilterDialog(final Filter filter) {
String message = getString(R.string.delete_filter, filter.text);
new MaterialAlertDialogBuilder(requireActivity())
.setMessage(message)
.setPositiveButton(R.string.delete, (dialog, which) -> {
if (DialogInterface.BUTTON_POSITIVE != which || null == mFilterList) {
return;
}
mFilterList.delete(filter);
if (null != mAdapter) {
mAdapter.notifyDataSetChanged();
}
updateView(true);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@Override
public int getFragmentTitle() {
return R.string.filter;
}
private static class FilterHolder extends RecyclerView.ViewHolder {
private final MaterialCheckBox checkbox;
private final TextView text;
private final ImageView delete;
public FilterHolder(View itemView) {
super(itemView);
checkbox = itemView.findViewById(R.id.checkbox);
text = itemView.findViewById(R.id.text);
delete = itemView.findViewById(R.id.delete);
}
}
private class AddFilterDialogHelper implements View.OnClickListener {
@Nullable
private AlertDialog mDialog;
@Nullable
private Spinner mSpinner;
@Nullable
private TextInputLayout mInputLayout;
@Nullable
private EditText mEditText;
public void setDialog(AlertDialog dialog) {
mDialog = dialog;
mSpinner = (Spinner) ViewUtils.$$(dialog, R.id.spinner);
mInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.text_input_layout);
mEditText = mInputLayout.getEditText();
View button = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (null != button) {
button.setOnClickListener(this);
}
}
@Override
public void onClick(View v) {
if (null == mFilterList || null == mDialog || null == mSpinner ||
null == mInputLayout || null == mEditText) {
return;
}
String text = mEditText.getText().toString().trim();
if (TextUtils.isEmpty(text)) {
mInputLayout.setError(getString(R.string.text_is_empty));
return;
} else {
mInputLayout.setError(null);
}
int mode = mSpinner.getSelectedItemPosition();
Filter filter = new Filter();
filter.mode = mode;
filter.text = text;
if (!mFilterList.add(filter)) {
mInputLayout.setError(getString(R.string.label_text_exist));
return;
} else {
mInputLayout.setError(null);
}
if (null != mAdapter) {
mAdapter.notifyDataSetChanged();
}
updateView(true);
mDialog.dismiss();
mDialog = null;
mSpinner = null;
mInputLayout = null;
mEditText = null;
}
}
private class FilterAdapter extends RecyclerView.Adapter<FilterHolder> {
private static final int TYPE_ITEM = 0;
private static final int TYPE_HEADER = 1;
@Override
public int getItemViewType(int position) {
if (null == mFilterList) {
return TYPE_ITEM;
}
if (mFilterList.get(position).mode == FilterList.MODE_HEADER) {
return TYPE_HEADER;
} else {
return TYPE_ITEM;
}
}
@NonNull
@Override
public FilterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
int layoutId;
switch (viewType) {
default:
case TYPE_ITEM:
layoutId = R.layout.item_filter;
break;
case TYPE_HEADER:
layoutId = R.layout.item_filter_header;
break;
}
return new FilterHolder(getLayoutInflater().inflate(layoutId, parent, false));
}
@Override
public void onBindViewHolder(@NonNull FilterHolder holder, int position) {
if (null == mFilterList) {
return;
}
Filter filter = mFilterList.get(position);
if (FilterList.MODE_HEADER == filter.mode) {
holder.text.setText(filter.text);
} else {
holder.checkbox.setText(filter.text);
holder.checkbox.setChecked(filter.enable);
holder.itemView.setOnClickListener(v -> {
mFilterList.trigger(filter);
//for updating delete line on filter text
if (mAdapter != null) {
mAdapter.notifyItemChanged(position);
}
});
holder.delete.setOnClickListener(v -> showDeleteFilterDialog(filter));
}
}
@Override
public int getItemCount() {
return null != mFilterList ? mFilterList.size() : 0;
}
@Override
public long getItemId(int position) {
if (mFilterList == null) {
return 0;
} else {
Filter filter = mFilterList.get(position);
if (filter.getId() != null) {
return (filter.text.hashCode() >> filter.mode) + filter.getId();
}
return filter.text.hashCode() >> filter.mode;
}
}
}
private class FilterList {
public static final int MODE_HEADER = -1;
private final EhFilter mEhFilter;
private final List<Filter> mTitleFilterList;
private final List<Filter> mUploaderFilterList;
private final List<Filter> mTagFilterList;
private final List<Filter> mTagNamespaceFilterList;
private Filter mTitleHeader;
private Filter mUploaderHeader;
private Filter mTagHeader;
private Filter mTagNamespaceHeader;
public FilterList() {
mEhFilter = EhFilter.getInstance();
mTitleFilterList = mEhFilter.getTitleFilterList();
mUploaderFilterList = mEhFilter.getUploaderFilterList();
mTagFilterList = mEhFilter.getTagFilterList();
mTagNamespaceFilterList = mEhFilter.getTagNamespaceFilterList();
}
public int size() {
int count = 0;
int size = mTitleFilterList.size();
count += 0 == size ? 0 : size + 1;
size = mUploaderFilterList.size();
count += 0 == size ? 0 : size + 1;
size = mTagFilterList.size();
count += 0 == size ? 0 : size + 1;
size = mTagNamespaceFilterList.size();
count += 0 == size ? 0 : size + 1;
return count;
}
private Filter getTitleHeader() {
if (null == mTitleHeader) {
mTitleHeader = new Filter();
mTitleHeader.mode = MODE_HEADER;
mTitleHeader.text = getString(R.string.filter_title);
}
return mTitleHeader;
}
private Filter getUploaderHeader() {
if (null == mUploaderHeader) {
mUploaderHeader = new Filter();
mUploaderHeader.mode = MODE_HEADER;
mUploaderHeader.text = getString(R.string.filter_uploader);
}
return mUploaderHeader;
}
private Filter getTagHeader() {
if (null == mTagHeader) {
mTagHeader = new Filter();
mTagHeader.mode = MODE_HEADER;
mTagHeader.text = getString(R.string.filter_tag);
}
return mTagHeader;
}
private Filter getTagNamespaceHeader() {
if (null == mTagNamespaceHeader) {
mTagNamespaceHeader = new Filter();
mTagNamespaceHeader.mode = MODE_HEADER;
mTagNamespaceHeader.text = getString(R.string.filter_tag_namespace);
}
return mTagNamespaceHeader;
}
public Filter get(int index) {
int size = mTitleFilterList.size();
if (0 != size) {
if (index == 0) {
return getTitleHeader();
} else if (index <= size) {
return mTitleFilterList.get(index - 1);
} else {
index -= size + 1;
}
}
size = mUploaderFilterList.size();
if (0 != size) {
if (index == 0) {
return getUploaderHeader();
} else if (index <= size) {
return mUploaderFilterList.get(index - 1);
} else {
index -= size + 1;
}
}
size = mTagFilterList.size();
if (0 != size) {
if (index == 0) {
return getTagHeader();
} else if (index <= size) {
return mTagFilterList.get(index - 1);
} else {
index -= size + 1;
}
}
size = mTagNamespaceFilterList.size();
if (0 != size) {
if (index == 0) {
return getTagNamespaceHeader();
} else if (index <= size) {
return mTagNamespaceFilterList.get(index - 1);
}
}
throw new IndexOutOfBoundsException();
}
public boolean add(Filter filter) {
return mEhFilter.addFilter(filter);
}
public void delete(Filter filter) {
mEhFilter.deleteFilter(filter);
}
public void trigger(Filter filter) {
mEhFilter.triggerFilter(filter);
}
}
}
@@ -0,0 +1,382 @@
/*
* 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.content.DialogInterface
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider
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.ehviewer.R
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.dao.Filter
import com.hippo.view.ViewTransition
import com.hippo.yorozuya.ViewUtils
class FilterFragment : BaseFragment() {
private var mViewTransition: ViewTransition? = null
private var mAdapter: FilterAdapter = FilterAdapter()
private var mFilterList: FilterList = FilterList()
private val mMenuProvider: MenuProvider = FilterMenuProvider()
inner class FilterMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_filter, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
val itemId = menuItem.itemId
if (itemId == R.id.action_tip) {
showTipDialog()
return true
}
return false
}
}
override fun onCreateView(
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
mViewTransition = ViewTransition(recyclerView, tip)
val fab = view.findViewById<FloatingActionButton>(R.id.fab)
val drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter)
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
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateView(false)
requireActivity().addMenuProvider(mMenuProvider)
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
if (0 == mFilterList.size()) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
}
override fun onDestroyView() {
super.onDestroyView()
mViewTransition = null
requireActivity().removeMenuProvider(mMenuProvider)
}
private fun showTipDialog() {
MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.filter)
.setMessage(R.string.filter_tip)
.setPositiveButton(android.R.string.ok, null)
.show()
}
private fun showAddFilterDialog() {
val dialog = MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.add_filter)
.setView(R.layout.dialog_add_filter)
.setPositiveButton(R.string.add, null)
.setNegativeButton(android.R.string.cancel, null)
.show()
AddFilterDialogHelper(dialog)
}
private fun showDeleteFilterDialog(filter: Filter) {
val message = getString(R.string.delete_filter, filter.text)
MaterialAlertDialogBuilder(requireActivity())
.setMessage(message)
.setPositiveButton(R.string.delete) { _: DialogInterface?, which: Int ->
if (DialogInterface.BUTTON_POSITIVE != which) {
return@setPositiveButton
}
mFilterList.delete(filter)
mAdapter.notifyDataSetChanged()
updateView(true)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun getFragmentTitle(): Int {
return R.string.filter
}
private class FilterHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val checkbox: MaterialCheckBox?
val text: TextView?
val delete: ImageView?
init {
checkbox = itemView.findViewById(R.id.checkbox)
text = itemView.findViewById(R.id.text)
delete = itemView.findViewById(R.id.delete)
}
}
private inner class AddFilterDialogHelper(dialog: AlertDialog) : View.OnClickListener {
private var mDialog: AlertDialog
private var mSpinner: TextInputLayout
private var mInputLayout: TextInputLayout
private var mEditText: EditText
private val mArray: Array<String> =
context!!.resources.getStringArray(R.array.filter_entries)
init {
mDialog = dialog
mSpinner = ViewUtils.`$$`(dialog, R.id.spinner) as TextInputLayout
mInputLayout = ViewUtils.`$$`(dialog, R.id.text_input_layout) as TextInputLayout
mEditText = mInputLayout.editText!!
val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
button?.setOnClickListener(this)
}
override fun onClick(v: View) {
val text1 = mSpinner.editText!!.text.toString()
if (TextUtils.isEmpty(text1)) {
mSpinner.error = getString(R.string.text_is_empty)
return
} else {
mSpinner.error = null
}
val text = mEditText.text.toString().trim { it <= ' ' }
if (TextUtils.isEmpty(text)) {
mInputLayout.error = getString(R.string.text_is_empty)
return
} else {
mInputLayout.error = null
}
val mode = mArray.indexOf(text1)
val filter = Filter()
filter.mode = mode
filter.text = text
if (!mFilterList.add(filter)) {
mInputLayout.error = getString(R.string.label_text_exist)
return
} else {
mInputLayout.error = null
}
mAdapter.notifyDataSetChanged()
updateView(true)
mDialog.dismiss()
}
}
private inner class FilterAdapter : RecyclerView.Adapter<FilterHolder>() {
override fun getItemViewType(position: Int): Int {
return if (mFilterList[position].mode == MODE_HEADER) {
TYPE_HEADER
} else {
TYPE_ITEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilterHolder {
val layoutId: Int = when (viewType) {
TYPE_ITEM -> R.layout.item_filter
TYPE_HEADER -> R.layout.item_filter_header
else -> R.layout.item_filter
}
return FilterHolder(layoutInflater.inflate(layoutId, parent, false))
}
override fun onBindViewHolder(holder: FilterHolder, position: Int) {
val filter = mFilterList[position]
if (MODE_HEADER == filter.mode) {
holder.text?.text = filter.text
} else {
holder.checkbox?.text = filter.text
holder.checkbox?.isChecked = filter.enable
holder.itemView.setOnClickListener {
mFilterList.trigger(filter)
//for updating delete line on filter text
mAdapter.notifyItemChanged(position)
}
holder.delete?.setOnClickListener { showDeleteFilterDialog(filter) }
}
}
override fun getItemCount(): Int {
return mFilterList.size()
}
override fun getItemId(position: Int): Long {
return run {
val filter = mFilterList[position]
if (filter.id != null) {
(filter.text.hashCode() shr filter.mode) + filter.id
} else (filter.text.hashCode() shr filter.mode).toLong()
}
}
}
private inner class FilterList {
private val mEhFilter: EhFilter = EhFilter.getInstance()
private val mTitleFilterList: List<Filter> = mEhFilter.titleFilterList
private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList
private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList
private val mTagNamespaceFilterList: List<Filter> = mEhFilter.tagNamespaceFilterList
private var mTitleHeader: Filter? = null
private var mUploaderHeader: Filter? = null
private var mTagHeader: Filter? = null
private var mTagNamespaceHeader: Filter? = null
fun size(): Int {
var count = 0
var size = mTitleFilterList.size
count += if (0 == size) 0 else size + 1
size = mUploaderFilterList.size
count += if (0 == size) 0 else size + 1
size = mTagFilterList.size
count += if (0 == size) 0 else size + 1
size = mTagNamespaceFilterList.size
count += if (0 == size) 0 else size + 1
return count
}
private val titleHeader: Filter
get() {
if (null == mTitleHeader) {
mTitleHeader = Filter()
mTitleHeader!!.mode = MODE_HEADER
mTitleHeader!!.text = getString(R.string.filter_title)
}
return mTitleHeader!!
}
private val uploaderHeader: Filter
get() {
if (null == mUploaderHeader) {
mUploaderHeader = Filter()
mUploaderHeader!!.mode = MODE_HEADER
mUploaderHeader!!.text = getString(R.string.filter_uploader)
}
return mUploaderHeader!!
}
private val tagHeader: Filter
get() {
if (null == mTagHeader) {
mTagHeader = Filter()
mTagHeader!!.mode = MODE_HEADER
mTagHeader!!.text = getString(R.string.filter_tag)
}
return mTagHeader!!
}
private val tagNamespaceHeader: Filter
get() {
if (null == mTagNamespaceHeader) {
mTagNamespaceHeader = Filter()
mTagNamespaceHeader!!.mode = MODE_HEADER
mTagNamespaceHeader!!.text = getString(R.string.filter_tag_namespace)
}
return mTagNamespaceHeader!!
}
operator fun get(index: Int): Filter {
var index1 = index
var size = mTitleFilterList.size
if (0 != size) {
index1 -= if (index1 == 0) {
return titleHeader
} else if (index1 <= size) {
return mTitleFilterList[index1 - 1]
} else {
size + 1
}
}
size = mUploaderFilterList.size
if (0 != size) {
index1 -= if (index1 == 0) {
return uploaderHeader
} else if (index1 <= size) {
return mUploaderFilterList[index1 - 1]
} else {
size + 1
}
}
size = mTagFilterList.size
if (0 != size) {
index1 -= if (index1 == 0) {
return tagHeader
} else if (index1 <= size) {
return mTagFilterList[index1 - 1]
} else {
size + 1
}
}
size = mTagNamespaceFilterList.size
if (0 != size) {
if (index1 == 0) {
return tagNamespaceHeader
} else if (index1 <= size) {
return mTagNamespaceFilterList[index1 - 1]
}
}
throw IndexOutOfBoundsException()
}
fun add(filter: Filter?): Boolean {
return mEhFilter.addFilter(filter)
}
fun delete(filter: Filter?) {
mEhFilter.deleteFilter(filter)
}
fun trigger(filter: Filter?) {
mEhFilter.triggerFilter(filter)
}
}
companion object {
private const val MODE_HEADER = -1
private const val TYPE_ITEM = 0
private const val TYPE_HEADER = 1
}
}
@@ -1,3 +1,21 @@
/*
* 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;
@@ -19,19 +37,15 @@ 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.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Hosts;
import com.hippo.ehviewer.R;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.List;
import java.util.Locale;
import rikka.core.res.ResourcesKt;
public class HostsFragment extends BaseFragment
implements View.OnClickListener {
@@ -67,12 +81,6 @@ public class HostsFragment extends BaseFragment
adapter = new HostsAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(requireActivity().getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(requireActivity(), 1));
decoration.setShowLastDivider(true);
recyclerView.addItemDecoration(decoration);
recyclerView.setHasFixedSize(true);
fab.setOnClickListener(this);
@@ -1,3 +1,21 @@
/*
* 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.annotation.SuppressLint;
@@ -1,37 +0,0 @@
package com.hippo.ehviewer.ui.fragment;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
/**
* Created by Mo10 on 2018/2/10.
*/
public class PrivacyFragment extends BasePreferenceFragment {
private static final String KEY_PATTERN_PROTECTION = "pattern_protection";
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.privacy_settings);
}
@Override
public void onStart() {
super.onStart();
Preference patternProtection = findPreference(KEY_PATTERN_PROTECTION);
patternProtection.setSummary(TextUtils.isEmpty(Settings.getSecurity()) ?
R.string.settings_privacy_pattern_protection_not_set :
R.string.settings_privacy_pattern_protection_set);
}
@Override
public int getFragmentTitle() {
return R.string.settings_privacy;
}
}
@@ -0,0 +1,47 @@
/*
* 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.os.Bundle
import androidx.preference.SwitchPreferenceCompat
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.scene.SecurityScene.Companion.isAuthenticationSupported
/**
* Created by Mo10 on 2018/2/10.
*/
class PrivacyFragment : BasePreferenceFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.privacy_settings)
}
override fun onStart() {
super.onStart()
if (!isAuthenticationSupported(requireContext())) {
Settings.putSecurity(false)
findPreference<SwitchPreferenceCompat>("require_unlock")?.isEnabled = false
findPreference<SwitchPreferenceCompat>("require_unlock")?.isChecked = false
}
}
override fun getFragmentTitle(): Int {
return R.string.settings_privacy
}
}
@@ -25,7 +25,7 @@ import com.hippo.ehviewer.R;
public class ReadFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.read_settings);
}
@@ -1,87 +0,0 @@
package com.hippo.ehviewer.ui.fragment;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.biometric.BiometricManager;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.widget.lockpattern.LockPatternUtils;
import com.hippo.widget.lockpattern.LockPatternView;
import com.hippo.yorozuya.ViewUtils;
public class SetSecurityFragment extends BaseFragment implements View.OnClickListener {
@Nullable
private LockPatternView mPatternView;
@Nullable
private View mCancel;
@Nullable
private View mSet;
@Nullable
private CheckBox mFingerprint;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.activity_set_security, container, false);
mPatternView = (LockPatternView) ViewUtils.$$(view, R.id.pattern_view);
mCancel = ViewUtils.$$(view, R.id.cancel);
mSet = ViewUtils.$$(view, R.id.set);
mFingerprint = (CheckBox) ViewUtils.$$(view, R.id.fingerprint_checkbox);
String pattern = Settings.getSecurity();
if (!TextUtils.isEmpty(pattern)) {
mPatternView.setPattern(LockPatternView.DisplayMode.Correct,
LockPatternUtils.stringToPattern(pattern));
}
if (BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) {
mFingerprint.setVisibility(View.VISIBLE);
mFingerprint.setChecked(Settings.getEnableFingerprint());
}
mCancel.setOnClickListener(this);
mSet.setOnClickListener(this);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mPatternView = null;
}
@Override
public void onClick(View v) {
if (v == mCancel) {
requireActivity().onBackPressed();
} else if (v == mSet) {
if (null != mPatternView && null != mFingerprint) {
String security;
if (mPatternView.getCellSize() <= 1) {
security = "";
} else {
security = mPatternView.getPatternString();
}
Settings.putSecurity(security);
Settings.putEnableFingerprint(mFingerprint.getVisibility() == View.VISIBLE &&
mFingerprint.isChecked() && !security.isEmpty());
}
requireActivity().onBackPressed();
}
}
@Override
public int getFragmentTitle() {
return R.string.set_pattern_protection;
}
}
@@ -1,3 +1,21 @@
/*
* 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.os.Bundle;
@@ -8,7 +26,7 @@ import com.hippo.ehviewer.R;
public class SettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_headers);
}
@@ -1,3 +1,21 @@
/*
* 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.annotation.SuppressLint;
@@ -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
@@ -22,22 +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.util.TypedValue;
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;
@@ -46,34 +41,23 @@ 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.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
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.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;
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
@@ -87,7 +71,6 @@ 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;
@@ -101,7 +84,6 @@ 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;
@@ -114,7 +96,6 @@ import java.util.LinkedList;
import java.util.List;
import rikka.core.res.ResourcesKt;
import rikka.recyclerview.RecyclerViewKt;
@SuppressLint("RtlHardcoded")
public class DownloadsScene extends ToolbarScene
@@ -378,12 +359,10 @@ public class DownloadsScene extends ToolbarScene
mRecyclerView.scrollToPosition(mInitPosition);
mInitPosition = -1;
}
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 4, 4, 4, 4, TypedValue.COMPLEX_UNIT_DIP);
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
mFastScroller.attachToRecyclerView(mRecyclerView);
HandlerDrawable handlerDrawable = new HandlerDrawable();
handlerDrawable.setColor(com.google.android.material.R.color.material_dynamic_neutral100);
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
mFastScroller.setHandlerDrawable(handlerDrawable);
mFastScroller.setOnDragHandlerListener(this);
@@ -395,91 +374,9 @@ public class DownloadsScene extends ToolbarScene
updateView();
guide();
return view;
}
private void guide() {
if (Settings.getGuideDownloadThumb() && null != mRecyclerView) {
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Settings.getGuideDownloadThumb()) {
guideDownloadThumb();
}
if (null != mRecyclerView) {
ViewUtils.removeOnGlobalLayoutListener(mRecyclerView.getViewTreeObserver(), this);
}
}
});
} else {
guideDownloadLabels();
}
}
private void guideDownloadThumb() {
MainActivity activity = getMainActivity();
if (null == activity || !Settings.getGuideDownloadThumb() || null == mLayoutManager || null == mRecyclerView) {
guideDownloadLabels();
return;
}
int position = mLayoutManager.findFirstCompletelyVisibleItemPositions(null)[0];
if (position < 0) {
guideDownloadLabels();
return;
}
RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(position);
if (null == holder) {
guideDownloadLabels();
return;
}
TapTargetView.showFor(requireActivity(),
TapTarget.forView(((DownloadHolder) holder).thumb,
getString(R.string.guide_download_thumb_title),
getString(R.string.guide_download_thumb_text))
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetDismissed(view, userInitiated);
Settings.putGuideDownloadThumb(false);
guideDownloadLabels();
}
});
}
private void guideDownloadLabels() {
MainActivity activity = getMainActivity();
if (null == activity || !Settings.getGuideDownloadLabels()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_download_labels_title),
getString(R.string.guide_download_labels_text))
.outerCircleColor(R.color.colorPrimary)
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.puttGuideDownloadLabels(false);
openDrawer(Gravity.RIGHT);
}
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@@ -645,26 +542,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));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
decoration.setShowLastDivider(true);
recyclerView.addItemDecoration(decoration);
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;
}
@@ -987,7 +871,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;
@@ -999,74 +883,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;
@@ -1120,36 +937,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);
@@ -1166,43 +962,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 {
@@ -1345,7 +1104,6 @@ public class DownloadsScene extends ToolbarScene
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list.get(index));
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
startScene(announcer);
} else if (start == v) {
Intent intent = new Intent(activity, DownloadService.class);
@@ -1403,8 +1161,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);
@@ -1541,4 +1298,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();
}
}
}
@@ -1,59 +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.view.View;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.transition.TransitionInflater;
import com.hippo.ehviewer.R;
import com.hippo.scene.TransitionHelper;
class EnterGalleryDetailTransaction implements TransitionHelper {
private final View mThumb;
public EnterGalleryDetailTransaction(View thumb) {
mThumb = thumb;
}
@Override
public boolean onTransition(Context context, FragmentTransaction transaction,
Fragment exit, Fragment enter) {
if (mThumb == null || !(enter instanceof GalleryDetailScene)) {
return false;
}
String transitionName = ViewCompat.getTransitionName(mThumb);
if (transitionName != null) {
exit.setSharedElementReturnTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
exit.setExitTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
enter.setSharedElementEnterTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
enter.setEnterTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
transaction.addSharedElement(mThumb, transitionName);
}
return true;
}
}
@@ -24,6 +24,7 @@ import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
@@ -45,8 +46,6 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.annotation.Implemented;
@@ -55,7 +54,6 @@ import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.R;
@@ -290,7 +288,8 @@ public class FavoritesScene extends BaseScene implements
updateSearchBar();
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView);
mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark));
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable);
mFabLayout.setExpanded(false, false);
mFabLayout.setAutoCancel(true);
@@ -311,41 +310,9 @@ public class FavoritesScene extends BaseScene implements
mHelper.firstRefresh();
}
guideCollections();
return view;
}
private void guideCollections() {
Activity activity = getMainActivity();
if (null == activity || !Settings.getGuideCollections()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_collections_title),
getString(R.string.guide_collections_text))
.outerCircleColor(R.color.colorPrimary)
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.putGuideCollections(false);
openDrawer(Gravity.RIGHT);
}
});
}
// keyword of mUrlBuilder, fav cat of mUrlBuilder, mFavCatArray.
// They changed, call it
private void updateSearchBar() {
@@ -446,12 +413,6 @@ public class FavoritesScene extends BaseScene implements
EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
decoration.setShowLastDivider(true);
recyclerView.addItemDecoration(decoration);
mDrawerAdapter = new FavDrawerAdapter(inflater);
recyclerView.setAdapter(mDrawerAdapter);
@@ -544,10 +505,6 @@ public class FavoritesScene extends BaseScene implements
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi);
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
View thumb;
if (null != (thumb = view.findViewById(R.id.thumb))) {
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
}
startScene(announcer);
}
}
@@ -898,7 +855,14 @@ public class FavoritesScene extends BaseScene implements
}
updateSearchBar();
mHelper.onGetPageData(taskId, result.pages, result.nextPage, result.galleryInfoList);
int pages = 0;
assert mUrlBuilder != null;
if (FavListUrlBuilder.isValidFavCat(mUrlBuilder.getFavCat()))
pages = CommonOperations.getPagesForFounds(mFavCountArray[mUrlBuilder.getFavCat()], 50);
else if (mUrlBuilder.getFavCat() == FavListUrlBuilder.FAV_CAT_ALL)
pages = CommonOperations.getPagesForFounds(mFavCountSum, 50);
mHelper.nextPg = result.nextPage;
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
if (mDrawerAdapter != null) {
mDrawerAdapter.notifyDataSetChanged();
@@ -1217,6 +1181,8 @@ public class FavoritesScene extends BaseScene implements
}
private class FavoritesHelper extends GalleryInfoContentHelper {
public int pgCounter = 0;
public String nextPg = null;
@Override
protected void getPageData(final int taskId, int type, int page) {
@@ -1224,6 +1190,7 @@ public class FavoritesScene extends BaseScene implements
if (null == activity || null == mUrlBuilder || null == mClient) {
return;
}
pgCounter = page;
if (mEnableModify) {
mEnableModify = false;
@@ -1263,7 +1230,7 @@ public class FavoritesScene extends BaseScene implements
url = mUrlBuilder.build();
}
mUrlBuilder.setIndex(page);
mUrlBuilder.setNext(nextPg);
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_MODIFY_FAVORITES);
request.setCallback(new GetFavoritesListener(getContext(),
@@ -1276,7 +1243,7 @@ public class FavoritesScene extends BaseScene implements
final String keyword = mUrlBuilder.getKeyword();
SimpleHandler.getInstance().post(() -> onGetFavoritesLocal(keyword, taskId));
} else {
mUrlBuilder.setIndex(page);
mUrlBuilder.setNext(nextPg);
String url = mUrlBuilder.build();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
@@ -1339,5 +1306,25 @@ public class FavoritesScene extends BaseScene implements
}
}
}
@Override
protected void onClearData() {
super.onClearData();
nextPg = null;
}
@Override
protected Parcelable saveInstanceState(Parcelable superState) {
Bundle bundle = (Bundle) super.saveInstanceState(superState);
bundle.putString(KEY_NEXT_PAGE, nextPg);
return bundle;
}
@Override
protected Parcelable restoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
nextPg = bundle.getString(KEY_NEXT_PAGE);
return super.restoreInstanceState(state);
}
}
}
@@ -24,9 +24,11 @@ import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Looper;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
@@ -34,7 +36,6 @@ import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
@@ -56,7 +57,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.UrlOpener;
@@ -71,7 +71,6 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.client.parser.VoteCommentParser;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import com.hippo.text.Html;
import com.hippo.text.URLImageGetter;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ExceptionUtils;
@@ -83,8 +82,6 @@ import com.hippo.widget.LinkifyTextView;
import com.hippo.widget.ObservedTextView;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ResourcesUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.StringUtils;
import com.hippo.yorozuya.ViewUtils;
@@ -94,7 +91,6 @@ import java.util.ArrayList;
import java.util.List;
import rikka.core.res.ResourcesKt;
import rikka.recyclerview.RecyclerViewKt;
public final class GalleryCommentsScene extends ToolbarScene
implements View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
@@ -176,7 +172,6 @@ public final class GalleryCommentsScene extends ToolbarScene
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_comments, container, false);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mEditPanel = ViewUtils.$$(view, R.id.edit_panel);
mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send);
@@ -189,14 +184,7 @@ public final class GalleryCommentsScene extends ToolbarScene
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();
@@ -212,18 +200,12 @@ public final class GalleryCommentsScene extends ToolbarScene
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context,
RecyclerView.VERTICAL, false));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL, ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
decoration.setShowLastDivider(true);
mRecyclerView.addItemDecoration(decoration);
mRecyclerView.setHasFixedSize(true);
// Cancel change animator
RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
if (itemAnimator instanceof DefaultItemAnimator) {
((DefaultItemAnimator) itemAnimator).setSupportsChangeAnimations(false);
}
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 0, 0, 0, 80, TypedValue.COMPLEX_UNIT_DIP);
mSendImage.setOnClickListener(this);
mFab.setOnClickListener(this);
@@ -331,11 +313,6 @@ public final class GalleryCommentsScene extends ToolbarScene
}
});
rv.setLayoutManager(new LinearLayoutManager(context));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL, ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
decoration.setPadding(ResourcesUtils.getAttrDimensionPixelOffset(context, androidx.appcompat.R.attr.dialogPreferredPadding));
rv.addItemDecoration(decoration);
rv.setClipToPadding(false);
builder.setView(rv).show();
}
@@ -873,9 +850,11 @@ public final class GalleryCommentsScene extends ToolbarScene
}
private CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) {
SpannableStringBuilder ssb = Html.fromHtml(comment.comment, new URLImageGetter(textView,
Spanned sp = Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY, new URLImageGetter(textView,
EhApplication.getConaco(context)), null);
SpannableStringBuilder ssb = new SpannableStringBuilder(sp);
if (0 != comment.id && 0 != comment.score) {
int score = comment.score;
String scoreString = score > 0 ? "+" + score : Integer.toString(score);
@@ -34,17 +34,17 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.text.Html;
import android.text.TextUtils;
import android.util.Pair;
import android.view.Gravity;
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.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RatingBar;
@@ -59,12 +59,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.transition.TransitionInflater;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.CircularProgressIndicator;
@@ -105,8 +101,6 @@ 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.scene.TransitionHelper;
import com.hippo.text.Html;
import com.hippo.text.URLImageGetter;
import com.hippo.util.AppHelper;
import com.hippo.util.ClipboardUtil;
@@ -137,7 +131,7 @@ import java.util.List;
import okhttp3.HttpUrl;
import rikka.core.res.ResourcesKt;
public class GalleryDetailScene extends BaseScene implements View.OnClickListener,
public class GalleryDetailScene extends ToolbarScene implements View.OnClickListener,
com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener,
View.OnLongClickListener {
@@ -178,8 +172,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
@Nullable
private TextView mCategory;
@Nullable
private ImageView mOtherActions;
@Nullable
private ViewGroup mActionGroup;
@Nullable
private TextView mDownload;
@@ -250,8 +242,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
private View mProgress;
@Nullable
private ViewTransition mViewTransition2;
@Nullable
private PopupMenu mPopupMenu;
@WholeLifeCircle
private int mDownloadState;
@Nullable
@@ -499,10 +489,62 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
outState.putInt(KEY_REQUEST_ID, mRequestId);
}
@Override
public int getMenuResId() {
return R.menu.scene_gallery_detail;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
@Override
public void onNavigationClick() {
onBackPressed();
super.onNavigationClick();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_open_in_other_app) {
String url = getGalleryDetailUrl(false);
Activity activity = getMainActivity();
if (null != url && null != activity) {
UrlOpener.openUrl(activity, url, false);
}
} else if (itemId == R.id.action_refresh) {
if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {
adjustViewVisibility(STATE_REFRESH, true);
request();
}
} else if (itemId == R.id.action_add_tag) {
if (mGalleryDetail == null) {
return false;
}
if (mGalleryDetail.apiUid < 0) {
showTip(R.string.sign_in_first, LENGTH_LONG);
return false;
}
EditTextDialogBuilder builder = new EditTextDialogBuilder(requireContext(), "", getString(R.string.action_add_tag_tip));
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.setTitle(R.string.action_add_tag)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener(v -> {
voteTag(builder.getText().trim(), 1);
dialog.dismiss();
});
}
return true;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateViewWithToolbar(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// Get download state
long gid = getGid();
if (gid != -1) {
@@ -517,12 +559,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
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);
}
});
View progressView = ViewUtils.$$(main, R.id.progress_view);
mTip = (TextView) ViewUtils.$$(main, R.id.tip);
mViewTransition = new ViewTransition(mainView, progressView, mTip);
@@ -541,14 +577,12 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
mTitle = (TextView) ViewUtils.$$(mHeader, R.id.title);
mUploader = (TextView) ViewUtils.$$(mHeader, R.id.uploader);
mCategory = (TextView) ViewUtils.$$(mHeader, R.id.category);
mOtherActions = (ImageView) ViewUtils.$$(mHeader, R.id.other_actions);
mActionGroup = (ViewGroup) ViewUtils.$$(mHeader, R.id.action_card);
mDownload = (TextView) ViewUtils.$$(mActionGroup, R.id.download);
mRead = (TextView) ViewUtils.$$(mActionGroup, R.id.read);
mUploader.setOnClickListener(this);
mCategory.setOnClickListener(this);
mOtherActions.setOnClickListener(this);
mDownload.setOnClickListener(this);
mDownload.setOnLongClickListener(this);
mRead.setOnClickListener(this);
@@ -558,7 +592,7 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
mBelowHeader = mainView.findViewById(R.id.below_header);
View belowHeader = mBelowHeader;
mInfo = ViewUtils.$$(belowHeader, R.id.info);
mInfo = ViewUtils.$$(mHeader, R.id.info);
mLanguage = (TextView) ViewUtils.$$(mInfo, R.id.language);
mPages = (TextView) ViewUtils.$$(mInfo, R.id.pages);
mSize = (TextView) ViewUtils.$$(mInfo, R.id.size);
@@ -628,11 +662,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
return view;
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -650,7 +679,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
mTitle = null;
mUploader = null;
mCategory = null;
mOtherActions = null;
mActionGroup = null;
mDownload = null;
mRead = null;
@@ -690,8 +718,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
mProgress = null;
mViewTransition2 = null;
mPopupMenu = null;
}
private boolean prepareData() {
@@ -804,19 +830,16 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
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
@@ -825,7 +848,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
default:
case STATE_INIT:
case STATE_FAILED:
setLightStatusBar(true);
// Show mFailedView
mViewTransition.showView(2, animation);
break;
@@ -1026,7 +1048,7 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
time.setText(ReadableTime.getTimeAgo(comment.time));
ObservedTextView c = v.findViewById(R.id.comment);
c.setMaxLines(5);
c.setText(Html.fromHtml(comment.comment,
c.setText(Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY,
new URLImageGetter(c, EhApplication.getConaco(context)), null));
v.setBackgroundColor(Color.TRANSPARENT);
}
@@ -1084,51 +1106,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
}
}
private void ensurePopMenu() {
if (mPopupMenu != null) {
return;
}
Context context = getContext();
AssertUtils.assertNotNull(context);
PopupMenu popup = new PopupMenu(context, mOtherActions, Gravity.TOP);
mPopupMenu = popup;
popup.getMenuInflater().inflate(R.menu.scene_gallery_detail, popup.getMenu());
popup.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.action_open_in_other_app) {
String url = getGalleryDetailUrl(false);
Activity activity = getMainActivity();
if (null != url && null != activity) {
UrlOpener.openUrl(activity, url, false);
}
} else if (itemId == R.id.action_refresh) {
if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {
adjustViewVisibility(STATE_REFRESH, true);
request();
}
} else if (itemId == R.id.action_add_tag) {
if (mGalleryDetail == null) {
return false;
}
if (mGalleryDetail.apiUid < 0) {
showTip(R.string.sign_in_first, LENGTH_LONG);
return false;
}
EditTextDialogBuilder builder = new EditTextDialogBuilder(context, "", getString(R.string.action_add_tag_tip));
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.setTitle(R.string.action_add_tag)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener(v -> {
voteTag(builder.getText().trim(), 1);
dialog.dismiss();
});
}
return true;
});
}
private void showSimilarGalleryList() {
GalleryDetail gd = mGalleryDetail;
if (null == gd) {
@@ -1203,11 +1180,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
if (request()) {
adjustViewVisibility(STATE_REFRESH, true);
}
} else if (mOtherActions == v) {
ensurePopMenu();
if (mPopupMenu != null) {
mPopupMenu.show();
}
} else if (mUploader == v) {
String uploader = getUploader();
if (TextUtils.isEmpty(uploader)) {
@@ -1552,17 +1524,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
@Override
public void onBackPressed() {
if (mViewTransition != null && mThumb != null &&
mViewTransition.getShownViewIndex() == 0 && mThumb.isShown()) {
int[] location = new int[2];
mThumb.getLocationInWindow(location);
// Only show transaction when thumb can be seen
if (location[1] + mThumb.getHeight() > 0) {
setTransitionName();
finish(new ExitTransaction(mThumb));
return;
}
}
finish();
}
@@ -1724,38 +1685,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
private @interface State {
}
private static class ExitTransaction implements TransitionHelper {
private final View mThumb;
public ExitTransaction(View thumb) {
mThumb = thumb;
}
@Override
public boolean onTransition(Context context,
FragmentTransaction transaction, Fragment exit, Fragment enter) {
if (!(enter instanceof GalleryListScene) && !(enter instanceof DownloadsScene) &&
!(enter instanceof FavoritesScene) && !(enter instanceof HistoryScene)) {
return false;
}
String transitionName = ViewCompat.getTransitionName(mThumb);
if (transitionName != null) {
exit.setSharedElementReturnTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
exit.setExitTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
enter.setSharedElementEnterTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
enter.setEnterTransition(
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
transaction.addSharedElement(mThumb, transitionName);
}
return true;
}
}
private static class GetGalleryDetailListener extends EhCallback<GalleryDetailScene, GalleryDetail> {
public GetGalleryDetailListener(Context context, int stageId, String sceneTag) {
@@ -29,7 +29,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.UrlOpener;
@@ -38,14 +37,10 @@ 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.LayoutUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import rikka.core.res.ResourcesKt;
import rikka.recyclerview.RecyclerViewKt;
public final class GalleryInfoScene extends ToolbarScene {
public static final String KEY_GALLERY_DETAIL = "gallery_detail";
@@ -165,16 +160,8 @@ public final class GalleryInfoScene extends ToolbarScene {
InfoAdapter adapter = new InfoAdapter();
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
int keylineMargin = context.getResources().getDimensionPixelOffset(R.dimen.keyline_margin);
decoration.setPadding(keylineMargin);
mRecyclerView.addItemDecoration(decoration);
mRecyclerView.setClipToPadding(false);
mRecyclerView.setHasFixedSize(true);
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
return view;
}
@@ -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;
@@ -50,32 +45,22 @@ 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.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
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.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.FavouriteStatusRouter;
@@ -113,7 +98,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;
@@ -614,7 +598,8 @@ public final class GalleryListScene extends BaseScene
mFabLayout.setOnExpandListener(this);
addAboveSnackView(mFabLayout);
mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark));
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable);
mSearchFab.setOnClickListener(this);
@@ -635,41 +620,9 @@ public final class GalleryListScene extends BaseScene
mHelper.firstRefresh();
}
guideQuickSearch();
return view;
}
private void guideQuickSearch() {
Activity activity = getMainActivity();
if (null == activity || !Settings.getGuideQuickSearch()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_quick_search_title),
getString(R.string.guide_quick_search_text))
.outerCircleColor(R.color.colorPrimary)
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.putGuideQuickSearch(false);
openDrawer(Gravity.RIGHT);
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -709,15 +662,10 @@ public final class GalleryListScene extends BaseScene
if (null == context) {
return;
}
final CheckBoxDialogBuilder builder = new CheckBoxDialogBuilder(
context, getString(R.string.add_quick_search_tip), getString(R.string.get_it), false);
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
builder.setMessage(R.string.add_quick_search_tip);
builder.setTitle(R.string.readme);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (builder.isChecked()) {
Settings.putQuickSearchTip(false);
}
showAddQuickSearchDialog(adapter, recyclerView, tip);
}).show();
builder.show();
}
@@ -793,25 +741,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));
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
LinearDividerItemDecoration.VERTICAL,
ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
LayoutUtils.dp2pix(context, 1));
decoration.setShowLastDivider(true);
recyclerView.addItemDecoration(decoration);
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) {
@@ -823,11 +759,9 @@ public final class GalleryListScene extends BaseScene
toolbar.setOnMenuItemClickListener(item -> {
int id = item.getItemId();
if (id == R.id.action_add) {
if (Settings.getQuickSearchTip()) {
showQuickSearchTipDialog(qsDrawerAdapter, recyclerView, tip);
} else {
showAddQuickSearchDialog(qsDrawerAdapter, recyclerView, tip);
}
showAddQuickSearchDialog(qsDrawerAdapter, recyclerView, tip);
} else if (id == R.id.action_help) {
showQuickSearchTipDialog(qsDrawerAdapter, recyclerView, tip);
}
return true;
});
@@ -843,22 +777,6 @@ public final class GalleryListScene extends BaseScene
return view;
}
private boolean checkDoubleClickExit() {
if (getStackIndex() != 0) {
return false;
}
long time = System.currentTimeMillis();
if (time - mPressBackTime > BACK_PRESSED_INTERVAL) {
// It is the last scene
mPressBackTime = time;
showTip(R.string.press_twice_exit, LENGTH_SHORT);
return true;
} else {
return false;
}
}
@Override
public void onBackPressed() {
if (null != mFabLayout && mFabLayout.isExpanded()) {
@@ -866,29 +784,21 @@ public final class GalleryListScene extends BaseScene
return;
}
boolean handle;
switch (mState) {
default:
case STATE_NORMAL:
handle = checkDoubleClickExit();
if (getStackIndex() == 0)
requireActivity().moveTaskToBack(false);
else
finish();
break;
case STATE_SIMPLE_SEARCH:
setState(STATE_NORMAL);
handle = true;
break;
case STATE_SEARCH:
setState(STATE_NORMAL);
handle = true;
break;
case STATE_SEARCH_SHOW_LIST:
setState(STATE_SEARCH);
handle = true;
break;
}
if (!handle) {
finish();
}
}
public boolean onItemClick(View view, int position) {
@@ -905,10 +815,6 @@ public final class GalleryListScene extends BaseScene
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi);
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
View thumb;
if (null != (thumb = view.findViewById(R.id.thumb))) {
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
}
startScene(announcer);
return true;
}
@@ -1465,7 +1371,7 @@ 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);
mHelper.onGetPageData(taskId, CommonOperations.getPagesForFounds(result.founds, 25), mHelper.pgCounter + 1, result.galleryInfoList);
}
}
@@ -1568,7 +1474,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;
@@ -1613,7 +1519,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;
@@ -1638,40 +1544,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};
@@ -1683,7 +1561,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);
@@ -1707,44 +1585,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 {
@@ -1851,15 +1691,20 @@ 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 (page != 0)
mUrlBuilder.setNextGid(minGid);
else
mUrlBuilder.setNextGid(0);
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) {
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_IMAGE_SEARCH);
@@ -1929,4 +1774,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();
}
}
}
@@ -36,6 +36,7 @@ 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.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
@@ -53,7 +54,6 @@ import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.Slider;
import com.hippo.widget.recyclerview.AutoGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
@@ -209,7 +209,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
return true;
}
int pages = mHelper.getPages();
if (pages > 0 && mHelper.canGoTo()) {
if (pages > 1 && mHelper.canGoTo()) {
GoToDialogHelper helper = new GoToDialogHelper(pages, mHelper.getPageForTop());
AlertDialog dialog = new MaterialAlertDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
@@ -418,8 +418,8 @@ public class GalleryPreviewsScene extends ToolbarScene {
((TextView) ViewUtils.$$(dialog, R.id.start)).setText(String.format(Locale.US, "%d", 1));
((TextView) ViewUtils.$$(dialog, R.id.end)).setText(String.format(Locale.US, "%d", mPages));
mSlider = (Slider) ViewUtils.$$(dialog, R.id.slider);
mSlider.setRange(1, mPages);
mSlider.setProgress(mCurrentPage + 1);
mSlider.setValueTo(mPages);
mSlider.setValue(mCurrentPage + 1);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
dialog.setOnDismissListener(this);
@@ -431,7 +431,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
return;
}
int page = mSlider.getProgress() - 1;
int page = (int) (mSlider.getValue() - 1);
if (page >= 0 && page < mPages && mHelper != null) {
mHelper.goTo(page);
if (mDialog != null) {
@@ -24,7 +24,6 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -37,20 +36,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
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;
@@ -86,7 +76,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rikka.recyclerview.RecyclerViewKt;
import rikka.core.res.ResourcesKt;
public class HistoryScene extends ToolbarScene {
@@ -207,17 +197,9 @@ public class HistoryScene extends ToolbarScene {
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()));
@@ -230,14 +212,11 @@ public class HistoryScene extends ToolbarScene {
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);
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 4, 4, 4, 4, TypedValue.COMPLEX_UNIT_DIP);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new HistoryItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(mRecyclerView);
mFastScroller.attachToRecyclerView(mRecyclerView);
HandlerDrawable handlerDrawable = new HandlerDrawable();
handlerDrawable.setColor(com.google.android.material.R.color.material_dynamic_neutral100);
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
mFastScroller.setHandlerDrawable(handlerDrawable);
updateLazyList();
@@ -347,10 +326,6 @@ public class HistoryScene extends ToolbarScene {
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);
View thumb;
if (null != (thumb = view.findViewById(R.id.thumb))) {
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
}
startScene(announcer);
return true;
}
@@ -509,7 +484,7 @@ public class HistoryScene extends ToolbarScene {
}
}
private static class HistoryHolder extends AbstractSwipeableItemViewHolder {
private static class HistoryHolder extends RecyclerView.ViewHolder {
public final View card;
public final LoadImageView thumb;
@@ -536,12 +511,6 @@ public class HistoryScene extends ToolbarScene {
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 {
@@ -577,8 +546,7 @@ public class HistoryScene extends ToolbarScene {
}
}
private class HistoryAdapter extends RecyclerView.Adapter<HistoryHolder>
implements SwipeableItemAdapter<HistoryHolder> {
private class HistoryAdapter extends RecyclerView.Adapter<HistoryHolder> {
private final LayoutInflater mInflater;
private final int mListThumbWidth;
@@ -656,55 +624,30 @@ public class HistoryScene extends ToolbarScene {
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 class HistoryItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final int mPosition;
protected SwipeResultActionClear(int position) {
mPosition = position;
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(0, ItemTouchHelper.LEFT);
}
@Override
protected void onPerformAction() {
super.onPerformAction();
if (null == mLazyList || null == mAdapter) {
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int mPosition = viewHolder.getBindingAdapterPosition();
if (null == mLazyList || mAdapter == null) {
return;
}
HistoryInfo info = mLazyList.get(mPosition);
EhDB.deleteHistoryInfo(info);
updateLazyList();
mAdapter.notifyDataSetChanged();
mAdapter.notifyItemRemoved(mPosition);
updateView(true);
}
}
@@ -1,193 +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.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.widget.lockpattern.LockPatternUtils;
import com.hippo.widget.lockpattern.LockPatternView;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.List;
import java.util.concurrent.Executors;
public class SecurityScene extends SolidScene implements
LockPatternView.OnPatternListener {
private static final int MAX_RETRY_TIMES = 5;
private static final String KEY_RETRY_TIMES = "retry_times";
@Nullable
private LockPatternView mPatternView;
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private BiometricPrompt.PromptInfo promptInfo;
private BiometricPrompt biometricPrompt;
private boolean canAuthenticate = false;
private int mRetryTimes;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = requireContext();
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
if (null != mSensorManager) {
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
if (null == savedInstanceState) {
mRetryTimes = MAX_RETRY_TIMES;
} else {
mRetryTimes = savedInstanceState.getInt(KEY_RETRY_TIMES);
}
canAuthenticate = Settings.getEnableFingerprint() &&
BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS;
biometricPrompt = new BiometricPrompt(this, Executors.newSingleThreadExecutor(), new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
startSceneForCheckStep(CHECK_STEP_SECURITY, getArguments());
finish();
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
});
promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.app_name))
.setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(false)
.build();
}
@Override
public void onDestroy() {
super.onDestroy();
mSensorManager = null;
mAccelerometer = null;
}
@Override
public void onResume() {
super.onResume();
if (canAuthenticate) {
startBiometricPrompt();
}
}
private void startBiometricPrompt() {
biometricPrompt.authenticate(promptInfo);
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_RETRY_TIMES, mRetryTimes);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_security, container, false);
if (canAuthenticate) {
view.setOnClickListener(v -> startBiometricPrompt());
}
mPatternView = (LockPatternView) ViewUtils.$$(view, R.id.pattern_view);
mPatternView.setOnPatternListener(this);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mPatternView = null;
}
@Override
public void onPatternStart() {
}
@Override
public void onPatternCleared() {
}
@Override
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
}
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
MainActivity activity = getMainActivity();
if (null == activity || null == mPatternView) {
return;
}
String enteredPatter = LockPatternUtils.patternToString(pattern);
String targetPatter = Settings.getSecurity();
if (ObjectUtils.equal(enteredPatter, targetPatter)) {
startSceneForCheckStep(CHECK_STEP_SECURITY, getArguments());
finish();
} else {
mPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
mRetryTimes--;
if (mRetryTimes <= 0) {
finish();
}
}
}
}
@@ -0,0 +1,91 @@
/*
* 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.scene
import android.content.Context
import android.os.Bundle
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.biometric.auth.AuthPromptCallback
import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
class SecurityScene : SolidScene() {
override fun needShowLeftDrawer(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAuthenticationSupported(requireContext()))
startAuthentication(getString(R.string.settings_privacy_require_unlock))
else
onSuccess()
}
private fun startAuthentication(
title: String,
subtitle: String? = null,
confirmationRequired: Boolean = true,
) {
startClass2BiometricOrCredentialAuthentication(
title = title,
subtitle = subtitle,
confirmationRequired = confirmationRequired,
executor = ContextCompat.getMainExecutor(requireContext()),
callback = AuthenticationCallback()
)
}
inner class AuthenticationCallback : AuthPromptCallback() {
override fun onAuthenticationSucceeded(
activity: FragmentActivity?,
result: BiometricPrompt.AuthenticationResult
) {
onSuccess()
}
override fun onAuthenticationError(
activity: FragmentActivity?,
errorCode: Int,
errString: CharSequence
) {
requireActivity().finish()
}
}
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
if (stackIndex == 0)
startSceneForCheckStep(CHECK_STEP_SECURITY, arguments)
finish()
}
}
@@ -87,12 +87,6 @@ public abstract class ToolbarScene extends BaseScene {
}
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
public int getMenuResId() {
return 0;
}
@@ -1,229 +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.widget;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.NumberUtils;
public class AdvanceSearchTable extends LinearLayout {
public static final int SNAME = 0x1;
public static final int STAGS = 0x2;
public static final int SDESC = 0x4;
public static final int STORR = 0x8;
public static final int STO = 0x10;
public static final int SDT1 = 0x20;
public static final int SDT2 = 0x40;
public static final int SH = 0x80;
public static final int SFL = 0x100;
public static final int SFU = 0x200;
public static final int SFT = 0x400;
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_ADVANCE_SEARCH = "advance_search";
private static final String STATE_KEY_MIN_RATING = "min_rating";
private static final String STATE_KEY_PAGE_FROM = "page_from";
private static final String STATE_KEY_PAGE_TO = "page_to";
private CheckBox mSname;
private CheckBox mStags;
private CheckBox mSdesc;
private CheckBox mStorr;
private CheckBox mSto;
private CheckBox mSdt1;
private CheckBox mSdt2;
private CheckBox mSh;
private CheckBox mSr;
private Spinner mMinRating;
private CheckBox mSp;
private EditText mSpf;
private EditText mSpt;
private CheckBox mSfl;
private CheckBox mSfu;
private CheckBox mSft;
public AdvanceSearchTable(Context context) {
super(context);
init(context);
}
public AdvanceSearchTable(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(Context context) {
setOrientation(LinearLayout.VERTICAL);
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.widget_advance_search_table, this);
ViewGroup row0 = (ViewGroup) getChildAt(0);
mSname = (CheckBox) row0.getChildAt(0);
mStags = (CheckBox) row0.getChildAt(1);
ViewGroup row1 = (ViewGroup) getChildAt(1);
mSdesc = (CheckBox) row1.getChildAt(0);
mStorr = (CheckBox) row1.getChildAt(1);
ViewGroup row2 = (ViewGroup) getChildAt(2);
mSto = (CheckBox) row2.getChildAt(0);
mSdt1 = (CheckBox) row2.getChildAt(1);
ViewGroup row3 = (ViewGroup) getChildAt(3);
mSdt2 = (CheckBox) row3.getChildAt(0);
mSh = (CheckBox) row3.getChildAt(1);
ViewGroup row4 = (ViewGroup) getChildAt(4);
mSr = (CheckBox) row4.getChildAt(0);
mMinRating = (Spinner) row4.getChildAt(1);
ViewGroup row5 = (ViewGroup) getChildAt(5);
mSp = (CheckBox) row5.getChildAt(0);
mSpf = (EditText) row5.getChildAt(1);
mSpt = (EditText) row5.getChildAt(3);
ViewGroup row7 = (ViewGroup) getChildAt(7);
mSfl = (CheckBox) row7.getChildAt(0);
mSfu = (CheckBox) row7.getChildAt(1);
mSft = (CheckBox) row7.getChildAt(2);
// Avoid java.lang.IllegalStateException: focus search returned a view that wasn't able to take focus!
mSpt.setOnEditorActionListener((v, actionId, event) -> {
View nextView = v.focusSearch(View.FOCUS_DOWN);
if (nextView != null) {
nextView.requestFocus(View.FOCUS_DOWN);
}
return true;
});
}
public int getAdvanceSearch() {
int advanceSearch = 0;
if (mSname.isChecked()) advanceSearch |= SNAME;
if (mStags.isChecked()) advanceSearch |= STAGS;
if (mSdesc.isChecked()) advanceSearch |= SDESC;
if (mStorr.isChecked()) advanceSearch |= STORR;
if (mSto.isChecked()) advanceSearch |= STO;
if (mSdt1.isChecked()) advanceSearch |= SDT1;
if (mSdt2.isChecked()) advanceSearch |= SDT2;
if (mSh.isChecked()) advanceSearch |= SH;
if (mSfl.isChecked()) advanceSearch |= SFL;
if (mSfu.isChecked()) advanceSearch |= SFU;
if (mSft.isChecked()) advanceSearch |= SFT;
return advanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
mSname.setChecked(NumberUtils.int2boolean(advanceSearch & SNAME));
mStags.setChecked(NumberUtils.int2boolean(advanceSearch & STAGS));
mSdesc.setChecked(NumberUtils.int2boolean(advanceSearch & SDESC));
mStorr.setChecked(NumberUtils.int2boolean(advanceSearch & STORR));
mSto.setChecked(NumberUtils.int2boolean(advanceSearch & STO));
mSdt1.setChecked(NumberUtils.int2boolean(advanceSearch & SDT1));
mSdt2.setChecked(NumberUtils.int2boolean(advanceSearch & SDT2));
mSh.setChecked(NumberUtils.int2boolean(advanceSearch & SH));
mSfl.setChecked(NumberUtils.int2boolean(advanceSearch & SFL));
mSfu.setChecked(NumberUtils.int2boolean(advanceSearch & SFU));
mSft.setChecked(NumberUtils.int2boolean(advanceSearch & SFT));
}
public int getMinRating() {
int position = mMinRating.getSelectedItemPosition();
if (mSr.isChecked() && position >= 0) {
return position + 2;
} else {
return -1;
}
}
public void setMinRating(int minRating) {
if (minRating >= 2 && minRating <= 5) {
mSr.setChecked(true);
mMinRating.setSelection(minRating - 2);
} else {
mSr.setChecked(false);
}
}
public int getPageFrom() {
if (mSp.isChecked()) {
return NumberUtils.parseIntSafely(mSpf.getText().toString(), -1);
}
return -1;
}
public void setPageFrom(int pageFrom) {
if (pageFrom > 0) {
mSpf.setText(Integer.toString(pageFrom));
mSp.setChecked(true);
} else {
mSp.setChecked(false);
mSpf.setText(null);
}
}
public int getPageTo() {
if (mSp.isChecked()) {
return NumberUtils.parseIntSafely(mSpt.getText().toString(), -1);
}
return -1;
}
public void setPageTo(int pageTo) {
if (pageTo > 0) {
mSpt.setText(Integer.toString(pageTo));
mSp.setChecked(true);
} else {
mSp.setChecked(false);
mSpt.setText(null);
}
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_ADVANCE_SEARCH, getAdvanceSearch());
state.putInt(STATE_KEY_MIN_RATING, getMinRating());
state.putInt(STATE_KEY_PAGE_FROM, getPageFrom());
state.putInt(STATE_KEY_PAGE_TO, getPageTo());
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setAdvanceSearch(savedState.getInt(STATE_KEY_ADVANCE_SEARCH));
setMinRating(savedState.getInt(STATE_KEY_MIN_RATING));
setPageFrom(savedState.getInt(STATE_KEY_PAGE_FROM));
setPageTo(savedState.getInt(STATE_KEY_PAGE_TO));
}
}
}
@@ -0,0 +1,189 @@
/*
* 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.widget
import android.content.Context
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.*
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
import com.hippo.yorozuya.NumberUtils
class AdvanceSearchTable @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private var mSname: CheckBox
private var mStags: CheckBox
private var mSdesc: CheckBox
private var mStorr: CheckBox
private var mSto: CheckBox
private var mSdt1: CheckBox
private var mSdt2: CheckBox
private var mSh: CheckBox
private var mSr: CheckBox
private var mMinRating: TextInputLayout
private var mSp: CheckBox
private var mSpf: EditText
private var mSpt: EditText
private var mSfl: CheckBox
private var mSfu: CheckBox
private var mSft: CheckBox
private val mArray: Array<String> = context.resources.getStringArray(R.array.search_min_rating)
init {
orientation = VERTICAL
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.widget_advance_search_table, this)
val row0 = getChildAt(0) as ViewGroup
mSname = row0.getChildAt(0) as CheckBox
mStags = row0.getChildAt(1) as CheckBox
val row1 = getChildAt(1) as ViewGroup
mSdesc = row1.getChildAt(0) as CheckBox
mStorr = row1.getChildAt(1) as CheckBox
val row2 = getChildAt(2) as ViewGroup
mSto = row2.getChildAt(0) as CheckBox
mSdt1 = row2.getChildAt(1) as CheckBox
val row3 = getChildAt(3) as ViewGroup
mSdt2 = row3.getChildAt(0) as CheckBox
mSh = row3.getChildAt(1) as CheckBox
val row4 = getChildAt(4) as ViewGroup
mSr = row4.getChildAt(0) as CheckBox
mMinRating = row4.getChildAt(1) as TextInputLayout
val row5 = getChildAt(5) as ViewGroup
mSp = row5.getChildAt(0) as CheckBox
mSpf = row5.getChildAt(1) as EditText
mSpt = row5.getChildAt(3) as EditText
val row7 = getChildAt(7) as ViewGroup
mSfl = row7.getChildAt(0) as CheckBox
mSfu = row7.getChildAt(1) as CheckBox
mSft = row7.getChildAt(2) as CheckBox
mSpt.setOnEditorActionListener { v: TextView, _: Int, _: KeyEvent? ->
val nextView = v.focusSearch(
FOCUS_DOWN
)
nextView?.requestFocus(FOCUS_DOWN)
true
}
mMinRating.isEnabled = mSr.isChecked
mSr.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
mMinRating.isEnabled = isChecked
}
mSpt.isEnabled = mSp.isChecked
mSpf.isEnabled = mSp.isChecked
mSp.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean ->
mSpt.isEnabled = isChecked
mSpf.isEnabled = isChecked
}
}
var advanceSearch: Int
get() {
var advanceSearch = 0
if (mSname.isChecked) advanceSearch = advanceSearch or SNAME
if (mStags.isChecked) advanceSearch = advanceSearch or STAGS
if (mSdesc.isChecked) advanceSearch = advanceSearch or SDESC
if (mStorr.isChecked) advanceSearch = advanceSearch or STORR
if (mSto.isChecked) advanceSearch = advanceSearch or STO
if (mSdt1.isChecked) advanceSearch = advanceSearch or SDT1
if (mSdt2.isChecked) advanceSearch = advanceSearch or SDT2
if (mSh.isChecked) advanceSearch = advanceSearch or SH
if (mSfl.isChecked) advanceSearch = advanceSearch or SFL
if (mSfu.isChecked) advanceSearch = advanceSearch or SFU
if (mSft.isChecked) advanceSearch = advanceSearch or SFT
return advanceSearch
}
set(advanceSearch) {
mSname.isChecked =
NumberUtils.int2boolean(advanceSearch and SNAME)
mStags.isChecked =
NumberUtils.int2boolean(advanceSearch and STAGS)
mSdesc.isChecked =
NumberUtils.int2boolean(advanceSearch and SDESC)
mStorr.isChecked = NumberUtils.int2boolean(advanceSearch and STORR)
mSto.isChecked = NumberUtils.int2boolean(advanceSearch and STO)
mSdt1.isChecked = NumberUtils.int2boolean(advanceSearch and SDT1)
mSdt2.isChecked = NumberUtils.int2boolean(advanceSearch and SDT2)
mSh.isChecked = NumberUtils.int2boolean(advanceSearch and SH)
mSfl.isChecked = NumberUtils.int2boolean(advanceSearch and SFL)
mSfu.isChecked = NumberUtils.int2boolean(advanceSearch and SFU)
mSft.isChecked = NumberUtils.int2boolean(advanceSearch and SFT)
}
var minRating: Int
get() {
val position = mArray.indexOf(mMinRating.editText!!.text.toString())
return if (mSr.isChecked && position >= 0) {
position + 2
} else {
-1
}
}
set(minRating) {
if (minRating in 2..5) {
mSr.isChecked = true
(mMinRating.editText!! as AutoCompleteTextView).setText(
mArray[minRating - 2],
false
)
} else {
mSr.isChecked = false
}
}
var pageFrom: Int
get() = if (mSp.isChecked) {
NumberUtils.parseIntSafely(mSpf.text.toString(), -1)
} else -1
set(pageFrom) {
if (pageFrom > 0) {
mSpf.setText(pageFrom.toString())
mSp.isChecked = true
} else {
mSp.isChecked = false
mSpf.text = null
}
}
var pageTo: Int
get() = if (mSp.isChecked) {
NumberUtils.parseIntSafely(mSpt.text.toString(), -1)
} else -1
set(pageTo) {
if (pageTo > 0) {
mSpt.setText(pageTo.toString())
mSp.isChecked = true
} else {
mSp.isChecked = false
}
}
companion object {
const val SNAME = 0x1
const val STAGS = 0x2
const val SDESC = 0x4
const val STORR = 0x8
const val STO = 0x10
const val SDT1 = 0x20
const val SDT2 = 0x40
const val SH = 0x80
const val SFL = 0x100
const val SFU = 0x200
const val SFT = 0x400
}
}
@@ -1,3 +1,21 @@
/*
* 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.widget;
import android.content.Context;
@@ -28,6 +28,7 @@ public class FixedThumb extends LoadImageView {
private float minAspect;
private float maxAspect;
private boolean alwaysCutAndScale;
public FixedThumb(Context context) {
super(context);
@@ -48,16 +49,16 @@ public class FixedThumb extends LoadImageView {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedThumb, defStyleAttr, defStyleRes);
minAspect = a.getFloat(R.styleable.FixedThumb_minAspect, 0.0f);
maxAspect = a.getFloat(R.styleable.FixedThumb_maxAspect, 0.0f);
alwaysCutAndScale = a.getBoolean(R.styleable.FixedThumb_alwaysCutAndScale, false);
a.recycle();
}
public void setFix(float minAspect, float maxAspect) {
this.minAspect = minAspect;
this.maxAspect = maxAspect;
}
@Override
public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) {
if (alwaysCutAndScale) {
setScaleType(ScaleType.CENTER_CROP);
return;
}
if (isTarget && drawable != null) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -29,8 +30,6 @@ import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.yorozuya.ViewUtils;
import rikka.core.res.ResourcesKt;
public class GalleryGuideView extends ViewGroup implements View.OnClickListener {
private final float[] mPoints = new float[3 * 4];
@@ -60,9 +59,9 @@ public class GalleryGuideView extends ViewGroup implements View.OnClickListener
}
private void init(Context context) {
mBgColor = ResourcesKt.resolveColor(context.getTheme(), R.attr.guideBackgroundColor);
mBgColor = Color.GRAY;
mPaint = new Paint();
mPaint.setColor(ResourcesKt.resolveColor(context.getTheme(), R.attr.guideTitleColor));
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(context.getResources().getDimension(R.dimen.gallery_guide_divider_width));
setOnClickListener(this);
@@ -31,8 +31,14 @@ import java.util.List;
import java.util.Map;
public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelper<GalleryInfo> {
public int maxGid = -1;
public int minGid = -1;
private static final String KEY_DATA_MAP = "data_map";
private static final String KEY_MAX_GID = "max_gid";
private static final String KEY_MIN_GID = "min_gid";
public static final String KEY_NEXT_PAGE = "next_page";
private final FavouriteStatusRouter.Listener listener;
@SuppressLint("UseSparseArrays")
private Map<Long, GalleryInfo> map = new HashMap<>();
@@ -59,6 +65,12 @@ public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelp
@Override
protected void onAddData(List<GalleryInfo> data) {
for (GalleryInfo info : data) {
if (maxGid == -1)
maxGid = (int) info.gid;
if (minGid == -1)
minGid = (int) info.gid;
maxGid = (int) Math.max(maxGid, info.gid);
minGid = (int) Math.min(minGid, info.gid);
map.put(info.gid, info);
}
}
@@ -78,6 +90,8 @@ public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelp
@Override
protected void onClearData() {
map.clear();
maxGid = -1;
minGid = -1;
}
@Override
@@ -88,6 +102,8 @@ public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelp
FavouriteStatusRouter router = EhApplication.getFavouriteStatusRouter();
int id = router.saveDataMap(map);
bundle.putInt(KEY_DATA_MAP, id);
bundle.putInt(KEY_MIN_GID, minGid);
bundle.putInt(KEY_MAX_GID, maxGid);
return bundle;
}
@@ -104,6 +120,8 @@ public abstract class GalleryInfoContentHelper extends ContentLayout.ContentHelp
this.map = map;
}
}
minGid = bundle.getInt(KEY_MIN_GID);
maxGid = bundle.getInt(KEY_MAX_GID);
return super.restoreInstanceState(state);
}
Loaded 100 of 482 files, more files were not shown because too many files have changed in this diff. Show more