Compare commits

...
134 Commits
Author SHA1 Message Date
txyyh 6973fa4538 Release 1.8.3.3
Releases / Build (push) Has been cancelled
2022-12-01 12:40:59 +00:00
renovate[bot] 20191c3f46 Update dependency gradle to v8.0-milestone-4 (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 19:03:44 +08:00
tarsin f084fec54e No more compress toolbar 2022-12-01 10:20:15 +08:00
tarsin 367e3b3a93 Res cleanup 2022-12-01 09:57:59 +08:00
tarsin 36383b6f47 Use Primary color for icons 2022-12-01 09:53:18 +08:00
tarsin 9ecd14820e Use Tertiary color for big icons 2022-12-01 09:48:45 +08:00
tarsin be552e92a1 Fix #476 2022-12-01 09:25:55 +08:00
tarsin 1bf676fd56 Upgrade AGP 2022-12-01 09:12:57 +08:00
tarsin 154a96b5fd Apply blur with themes 2022-12-01 08:52:27 +08:00
FooIbar 443c384f41 Fixup! Migrate to java.time 2022-11-29 08:34:53 +08:00
tarsin 85c7829636 Robust for partially decode, Resolve #469, Resolve #470 2022-11-29 08:33:10 +08:00
tarsin 68e3bb8d66 Fix backpresscallback 2022-11-28 11:58:17 +08:00
tarsin 63b2e52eaf DomainFronting tweaks 2022-11-28 10:03:30 +08:00
tarsin c7492f3faf Rename .java to .kt 2022-11-28 10:03:30 +08:00
tarsin 9052343ef7 Upgrade build-tools 2022-11-28 10:03:30 +08:00
tarsin 00c09e4a1a Remove outdated workaround 2022-11-28 10:03:30 +08:00
FooIbar 086c0d9f28 Migrate to java.time 2022-11-28 07:00:54 +08:00
FooIbar 3ca0189f28 Drop Jetifier 2022-11-28 07:00:54 +08:00
tarsin 8f5a2ba85d Relax limit since we could scale it 2022-11-26 15:39:55 +08:00
tarsin 1ab9811d7a Don't let decoded image resolution out of screen resolution, Resolve #460 2022-11-26 15:10:03 +08:00
tarsin 0b720848ba Unpin an ip 2022-11-26 13:24:14 +08:00
tarsin eebd95e700 Fix download margin 2022-11-26 13:18:39 +08:00
tarsin 913e02f0fa translations tweaks 2022-11-26 13:15:09 +08:00
tarsin 10fdab2473 Show comments by default 2022-11-26 13:10:34 +08:00
小白-白 26710761ed Fix wrong timezone 2022-11-26 09:32:47 +08:00
tarsin 17bf6ac12b Delete 1.7 from renovate bot 2022-11-26 00:31:36 +08:00
tarsin 6a01b7729d fixup! 039e817002 2022-11-25 11:12:00 +08:00
小白-白 039e817002 Support custom preview number, Resolve #455 2022-11-25 10:24:52 +08:00
小白-白 3ea1e1914a Support hide comments, Resolve $454 2022-11-25 10:13:14 +08:00
tarsin e6c5152123 Allow obfuscate some parts 2022-11-25 10:06:16 +08:00
tarsin 7bdaf186f3 Drop Compat for Webview 2022-11-25 10:06:16 +08:00
Edward Wu b6b4e826cb Ehviewer: Translate: Update zh-rTW Translate (11/25)
* Update string from 1d8ab51 (Res cleanup)

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-11-25 09:54:24 +08:00
小白-白 949f3cb246 Fix jump on tag and uploader page 2022-11-24 12:03:26 +08:00
FooIbar 501855a8c0 Replace "..." with ellipsis character 2022-11-23 00:07:36 +08:00
FooIbar 44634be13d Remove untranslatable string 2022-11-23 00:07:36 +08:00
FooIbar 7c718df1af Use app:showAsAction with appcompat 2022-11-23 00:07:36 +08:00
FooIbar 16cca9cbbd Remove redundant namespace 2022-11-23 00:07:36 +08:00
FooIbar b715320298 android:forceDarkAllowed requires API 29 2022-11-23 00:07:36 +08:00
FooIbar d31d9eb137 WRITE_EXTERNAL_STORAGE is not required on API 29 2022-11-23 00:07:36 +08:00
tarsin b53c29c035 Fix fab position 2022-11-22 21:26:13 +08:00
tarsin d3bf20bee6 Refresh tokens, fuck github 2022-11-22 19:57:00 +08:00
tarsin cff445ddf5 Use latest mdc for searchbar migration 2022-11-22 19:47:30 +08:00
tarsin 877d96a50a Release 1.8.3.2
Releases / Build (push) Has been cancelled
2022-11-22 16:19:20 +08:00
tarsin 12863dfa21 Fixup! Fix comments insets 2022-11-22 15:10:34 +08:00
tarsin 959959d880 Fixup! Fix comments insets 2022-11-22 15:06:51 +08:00
tarsin cd676eabfc Mitigate CCP's sanctions 2022-11-22 14:57:58 +08:00
tarsin 0a3e95c8d4 Rename .java to .kt 2022-11-22 14:57:58 +08:00
tarsin 35b483e655 Harmonize shortcut color 2022-11-22 13:18:56 +08:00
tarsin 7dae7b72f4 Checkout libarchive to latest 2022-11-22 12:43:28 +08:00
tarsin 620f5df241 Checkout liblzma to v5.2.8 2022-11-22 12:38:07 +08:00
tarsin d5ddb6eb8c Fix comments insets 2022-11-22 11:52:23 +08:00
txyyh 33c9b9d4e4 Release 1.8.3.1
Releases / Build (push) Has been cancelled
2022-11-21 12:39:03 +00:00
txyyh 216cafbe54 Revert "Update README.md"
This reverts commit a37c676d96.
2022-11-21 12:34:17 +00:00
txyyh 72a644bd3b Remove Android version note 2022-11-21 12:10:44 +00:00
tarsin 7008f0bc89 Better solution for #440 2022-11-21 20:09:24 +08:00
tarsin 289d3ea287 Resolve #440 2022-11-21 19:47:21 +08:00
tarsin 92ae157f00 Update en.md 2022-11-21 16:56:11 +08:00
tarsin 9800f7c01b Update zh-tw.md 2022-11-21 16:55:44 +08:00
tarsin 8281af4590 Update zh-cn.md 2022-11-21 16:55:24 +08:00
tarsin d609e5e113 Update README.md 2022-11-21 16:55:01 +08:00
tarsin 238d769f27 Release 1.8.3.0
Releases / Build (push) Has been cancelled
Just stop spamming for something like "I can install", If U still can't install then please SMASH YOUR DEVICE OR REPLACE YOUR GODDAMN SYSTEM
2022-11-21 16:48:50 +08:00
tarsin 1e648cc6b4 Nullability checks
extractToByteBuffer may return null object for some reason
2022-11-21 12:15:23 +08:00
tarsin 7ac20d59fc Use DirectByteBuffer
Seems my mimalloc(I use it as Android's system allocator) doesn't like release memory to system since I replace MADV_DONTNEED with MADV_FREE
https://github.com/AcmeUI/android_external_mimalloc
but there is no memleak I sure
2022-11-21 12:11:35 +08:00
tarsin 1d8ab51a22 Res cleanup 2022-11-20 21:06:11 +08:00
tarsin cdfb3b74b3 Code cleanup 2022-11-20 20:57:16 +08:00
小白-白 6f2868f7c3 Support select all on fav page (#437) 2022-11-20 19:18:43 +08:00
tarsin f3061fa497 Kotlin 1.8 and Java 19 2022-11-20 19:09:27 +08:00
FooIbar 468cce5f3d Remove fix thumb url (#431) 2022-11-20 10:03:21 +08:00
小白-白 bfaba80d3b Fix backup online fav list to local 2022-11-20 09:09:36 +08:00
小白-白 2ca4a2af86 Restrict datepicker ranges with validators 2022-11-20 09:09:36 +08:00
tarsin 7d0b245d3d Unroll loops 2022-11-20 00:09:00 +08:00
tarsin 96fcf69bdd Guard blur method 2022-11-19 23:01:44 +08:00
tarsin 63d95b1265 Guard some methods 2022-11-19 22:57:34 +08:00
tarsin d173e83e01 Resolve #421 2022-11-19 22:24:38 +08:00
tarsin 54d11dea95 Initial migrate to ImageDecoder Java api, and downgrade minsdk to 28 2022-11-19 22:17:08 +08:00
tarsin fbdab389c4 Let's target JVM 18 2022-11-17 23:39:06 +08:00
tarsin 3dd3bad362 Resolve #393 2022-11-17 21:54:05 +08:00
tarsin 65a9a4b4f2 Make Settings appbar liftonscroll 2022-11-17 21:34:28 +08:00
tarsin 5f6cf5550d Rename .java to .kt 2022-11-17 21:34:28 +08:00
tarsin 673dad4032 Fix comment send ui color 2022-11-17 19:33:12 +08:00
tarsin 5e45ec9a9c Restrict datepicker ranges 2022-11-17 18:11:02 +08:00
tarsin 306eb9d23c Bump version
Releases / Build (push) Has been cancelled
2022-11-17 12:36:19 +08:00
tarsin 158218e501 No jump menu when cannot jump pages 2022-11-17 12:22:19 +08:00
tarsin d773332b74 Use stable version of mdc 2022-11-17 12:21:59 +08:00
tarsin 5dc2eb0062 Expand title on CollapsingToolbarLayout 2022-11-17 11:31:40 +08:00
tarsin bee7febafc Use compressed toolbar 2022-11-17 00:45:38 +08:00
tarsin f49cc2d498 Fix GalleryCommentsScene fab and edittext render out of screen 2022-11-17 00:34:10 +08:00
tarsin 3f4d2b75fc Correctly use attach behaviour, not padding 2022-11-16 23:48:20 +08:00
tarsin af8733c2d1 Explicitly set LiftOnScrollTargetView 2022-11-16 22:46:24 +08:00
tarsin 545eed98a7 Support lift Appbar when scroll, use CollapsingAppbar on gallery detail for long title 2022-11-16 21:50:31 +08:00
tarsin 2da436189a Allow overscroll on GalleryDetailScene, also set title on appbar, Resolve #277 2022-11-16 17:26:27 +08:00
tarsin 906ea11733 Rename .java to .kt 2022-11-16 17:26:27 +08:00
小白-白 47c5629f7d Fix fav page load after jump 2022-11-16 11:52:41 +08:00
tarsin a37c676d96 Update README.md 2022-11-15 22:36:16 +08:00
tarsin d9981f15f2 Bump version
Releases / Build (push) Has been cancelled
2022-11-15 22:19:21 +08:00
tarsin 27b75746fd Simplify clipboardutils 2022-11-15 22:17:12 +08:00
tarsin ee80a3a841 Copy to Clipboard refactor 2022-11-15 22:05:26 +08:00
tarsin f8d639054f Just use normal rv 2022-11-15 21:48:22 +08:00
tarsin 7f76e58e4e Move GalleryInfoBottomSheet out of scene package to allow obfuscate and repackage 2022-11-15 21:46:00 +08:00
tarsin 1b326a86a2 Convert GalleryInfoScene to BottomSheet, Resolve #354 2022-11-15 21:38:27 +08:00
tarsin 607bf4c67f Use inverse color on snackbar, #326 2022-11-15 15:30:44 +08:00
小白-白 3e4836c91e Support favorites prev page load 2022-11-15 12:54:41 +08:00
小白-白 dae8f21388 Support prev page load 2022-11-15 12:54:41 +08:00
小白-白 84b328b60b Init prev page load 2022-11-15 12:54:41 +08:00
小白-白 b98d55b444 Hide jump button on local fav cat 2022-11-15 12:54:41 +08:00
小白-白 ee9413e54a Fix favorite page jump 2022-11-15 12:54:41 +08:00
小白-白 76dedce5da Remove unavailable search options 2022-11-15 12:54:41 +08:00
tarsin 37f99ae706 Fuck off the "Press twice to exit"
It can't be more stupid on our modern gesture navigation
2022-11-14 19:34:37 +08:00
tarsin aee58d967d Kotlin 1.7.21 2022-11-14 14:42:41 +08:00
tarsin 5ecabeb045 Rearrange gallery actions, resolve #413 2022-11-14 14:29:02 +08:00
tarsin 408c142aad Haptic feedback for slider, #408 2022-11-14 14:09:54 +08:00
tarsin a56d541380 fixup! Adjust madvise usage 2022-11-10 15:28:06 +08:00
tarsin ae79417845 Adjust madvise usage 2022-11-10 15:21:17 +08:00
小白-白 010daa583e Remove unavailable advanced search options 2022-11-10 12:36:08 +08:00
renovate[bot] 3034957c99 Update room_version to v2.5.0-beta02 (1.8.x.x) (#402)
Update room_version to v2.5.0-beta02

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 04:36:48 +00:00
txyyh a2cb6444f4 Revert "Remove preview count limit #332 (#333)"
This reverts commit 783fa10157.
2022-11-07 10:28:34 +00:00
tarsin 8a144eabca Reset ContentLayout changes
This component is quite a shit, further we must rewrite it to better adapt new page-off gid-based load design
2022-11-07 18:26:27 +08:00
tarsin 75c2a3b0ec Tweak HistoryScene with modern kotlin 2022-11-06 23:19:15 +08:00
tarsin beec02a69b Fix #370 2022-11-06 21:59:08 +08:00
tarsin df78b45f69 Initial support for jump to certain date 2022-11-06 14:58:05 +08:00
tarsin e6a3c8f165 Rename .java to .kt 2022-11-06 14:58:05 +08:00
tarsin 4d948e93a3 Datepicker stub 2022-11-06 13:23:49 +08:00
tarsin ece082727b Release 1.8.3.0-alpha04
Releases / Build (push) Has been cancelled
2022-11-06 13:02:55 +08:00
tarsin 5b5040f4af No gallery found no more a exception 2022-11-06 13:02:05 +08:00
tarsin d9d88e690b Fix favourite list load first 2022-11-06 12:57:51 +08:00
tarsin bda123fc39 Build with JDK 19
Gradle 8.0.0-M3 should be able to work with JDK19
2022-11-06 00:22:59 +08:00
tarsin 2200bb9b45 Upgrade java compatibility to Java 17 2022-11-06 00:21:25 +08:00
195 changed files with 3916 additions and 6121 deletions

No files matched your search

+2 -2
View File
@@ -17,11 +17,11 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 18
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 18
java-version: 19
- name: Install NDK
run: |
+2 -2
View File
@@ -12,11 +12,11 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 18
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 18
java-version: 19
- name: Install NDK
run: |
+12 -12
View File
@@ -15,11 +15,11 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 18
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 18
java-version: 19
- name: Install NDK
run: |
@@ -32,11 +32,11 @@ jobs:
- name: Rename Apks
run: |
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal-A12+.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86-A12+.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
- name: Releases
uses: softprops/action-gh-release@v1
@@ -44,11 +44,11 @@ jobs:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/EhViewer-universal-A12+.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
app/build/outputs/apk/release/EhViewer-x86-A12+.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
app/build/outputs/apk/release/EhViewer-universal.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
app/build/outputs/apk/release/EhViewer-x86_64.apk
app/build/outputs/apk/release/EhViewer-x86.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
- name: Collect Apks
run: |
-2
View File
@@ -68,8 +68,6 @@ with Material Design 3 and Dynamic Color Support
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="art/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="art/get-it-on-fdroid.svg" width="200px"/></a>
App 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)
+17 -13
View File
@@ -6,7 +6,7 @@ plugins {
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
buildToolsVersion "33.0.1"
ndkVersion "25.1.8937393"
splits {
@@ -29,10 +29,10 @@ android {
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 31
minSdkVersion 28
targetSdkVersion 33
versionCode 180017
versionName "1.8.3.0-alpha03"
versionCode 180024
versionName "1.8.3.3"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
@@ -43,12 +43,16 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_19
targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '19'
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
]
}
lint {
@@ -82,11 +86,12 @@ android {
}
buildFeatures {
buildConfig true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
}
namespace 'com.hippo.ehviewer'
@@ -112,7 +117,7 @@ dependencies {
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
def compose_version = "1.3.0"
def compose_version = "1.3.1"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
@@ -120,7 +125,7 @@ dependencies {
implementation "androidx.compose.runtime:runtime-saveable:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.0.0"
implementation "androidx.compose.material3:material3:1.0.1"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha04"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@@ -157,7 +162,7 @@ dependencies {
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
def room_version = "2.5.0-beta01"
def room_version = "2.5.0-beta02"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
@@ -169,10 +174,9 @@ dependencies {
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.5.0'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.8.0-alpha02'
implementation 'com.google.android.material:material:1.8.0-SNAPSHOT'
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp'
-4
View File
@@ -14,10 +14,6 @@
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
+4
View File
@@ -28,6 +28,10 @@
<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" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
+1 -1
View File
@@ -1,7 +1,7 @@
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -flto=thin \
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
+41 -32
View File
@@ -24,6 +24,7 @@
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <syscall.h>
#include <jni.h>
#include <android/log.h>
@@ -32,12 +33,8 @@
#include <archive_entry.h>
#define LOG_TAG "libarchive_wrapper"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
#include "ehviewer.h"
typedef struct {
int using;
@@ -53,7 +50,7 @@ static archive_ctx **ctx_pool;
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
static jlong archiveSize = 0;
static size_t archiveSize = 0;
const char supportExt[9][6] = {
"jpeg",
@@ -108,8 +105,12 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
free(ctx);
return -ENOMEM;
}
archive_read_support_format_all(ctx->arc);
archive_read_support_filter_all(ctx->arc);
archive_read_support_format_tar(ctx->arc);
archive_read_support_format_7zip(ctx->arc);
archive_read_support_format_rar5(ctx->arc);
archive_read_support_format_zip(ctx->arc);
archive_read_support_filter_gzip(ctx->arc);
archive_read_support_filter_xz(ctx->arc);
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(ctx->arc, passwd);
@@ -195,15 +196,16 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
JNIEXPORT jint JNICALL
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
archiveAddr = mmap64(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%d", "mmap64 failed with errno ", errno);
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
return 0;
}
archiveSize = size;
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
pthread_mutex_init(&ctx_lock, 0);
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
@@ -231,42 +233,43 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
need_encrypt = false;
}
// madvise for readahead optimization
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR:
case ARCHIVE_FORMAT_RAR_V5:
madvise(archiveAddr, archiveSize, MADV_SEQUENTIAL | MADV_WILLNEED);
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
}
archive_release_ctx(ctx);
return r;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_UriArchiveAccessor_extractToAddr(JNIEnv *env, jobject thiz, jint index) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
JNIEXPORT jobject JNICALL
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size + sizeof(long long));
void *buffer = malloc(size);
if (!buffer) {
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
}
*(long long *) buffer = size;
ret = archive_read_data(ctx->arc, buffer + sizeof(long long), size);
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (jlong) buffer;
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
@@ -277,8 +280,8 @@ Java_com_hippo_UriArchiveAccessor_extractToAddr(JNIEnv *env, jobject thiz, jint
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(passwd);
@@ -291,14 +294,14 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
(void) thiz; /* UNUSED */
EH_UNUSED(thiz);
struct archive_entry *entry;
archive_ctx *ctx;
jboolean ret = true;
@@ -328,8 +331,8 @@ Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jst
JNIEXPORT jstring JNICALL
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
@@ -342,8 +345,8 @@ Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint in
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
(void) env; /* UNUSED */
(void) thiz; /* UNUSED */
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
@@ -351,4 +354,10 @@ Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint in
archive_read_data_into_fd(ctx->arc, fd);
ctx->using = 0;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
free(addr);
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EHVIEWER_EHVIEWER_H
#define EHVIEWER_EHVIEWER_H
#define EH_UNUSED(x) (void)x
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
#define madvise_log_if_error(addr, len, advice) \
if (madvise(addr, len, advice)) \
LOGE("%s%p%s%zu%s%d%s%s%s", "madvise addr:", addr, "len:", len, "with advice ", advice, " failed with error: ", strerror(errno), ", Ignored")
#endif /* EHVIEWER_EHVIEWER_H */
+26 -202
View File
@@ -20,58 +20,21 @@
#include <stdlib.h>
#include <string.h>
#include <android/bitmap.h>
#include <android/data_space.h>
#include <android/imagedecoder.h>
#include <android/log.h>
#include <GLES2/gl2.h>
#include <jni.h>
#define TAG "ImageDecoder_wrapper"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, TAG ,__VA_ARGS__)
#include "ehviewer.h"
#define IMAGE_TILE_MAX_SIZE (512 * 512)
static char tile_buffer[IMAGE_TILE_MAX_SIZE * 4];
typedef struct {
AImageDecoder *decoder;
void *srcBuffer;
size_t stride;
int fd;
} SOURCE;
typedef struct {
int32_t width;
int32_t height;
void *buffer;
int bufferLen;
bool isAnimated;
bool alpha;
int delay;
SOURCE *src;
} IMAGE;
static void recycleSource(SOURCE *src) {
if (!src)
return;
AImageDecoder_delete(src->decoder);
free(src->srcBuffer);
free(src);
}
static void recycle(IMAGE *image) {
if (!image)
return;
recycleSource(image->src);
free(image->buffer);
free(image);
}
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
@@ -159,138 +122,40 @@ bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
return true;
}
void decodeCommon(IMAGE *image, SOURCE *src) {
image->src = src;
AImageDecoder *decoder = src->decoder;
image->isAnimated = AImageDecoder_isAnimated(decoder);
AImageDecoder_setDataSpace(decoder, ADATASPACE_SRGB);
AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888);
const AImageDecoderHeaderInfo *headerInfo = AImageDecoder_getHeaderInfo(decoder);
image->height = AImageDecoderHeaderInfo_getHeight(headerInfo);
image->width = AImageDecoderHeaderInfo_getWidth(headerInfo);
image->alpha =
AImageDecoderHeaderInfo_getAlphaFlags(headerInfo) == ANDROID_BITMAP_FLAGS_ALPHA_PREMUL;
src->stride = AImageDecoder_getMinimumStride(decoder);
image->bufferLen = (int) (image->height * src->stride);
image->buffer = malloc(image->bufferLen);
if (image->isAnimated) {
AImageDecoder_setInternallyHandleDisposePrevious(decoder, false);
AImageDecoderFrameInfo *frameInfo = AImageDecoderFrameInfo_create();
AImageDecoder_getFrameInfo(decoder, frameInfo);
image->delay = (int) AImageDecoderFrameInfo_getDuration(frameInfo) / 1000000;
AImageDecoderFrameInfo_delete(frameInfo);
}
AImageDecoder_decodeImage(decoder, image->buffer, src->stride, image->bufferLen);
if (!image->isAnimated) {
recycleSource(image->src);
image->src = NULL;
}
}
IMAGE *createFromFd(int fd) {
AImageDecoder *decoder = NULL;
if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS)
return NULL;
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->fd = fd;
decodeCommon(image, src);
return image;
}
IMAGE *createFromAddr(void *addr, long long size) {
AImageDecoder *decoder;
AImageDecoder_createFromBuffer((const void *) addr + sizeof(long long), size, &decoder);
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->srcBuffer = addr;
decodeCommon(image, src);
return image;
}
IMAGE *create(int32_t width, int32_t height, const void *data) {
IMAGE *plain = calloc(1, sizeof(IMAGE));
void *buffer = NULL;
size_t length = width * height * 4;
buffer = malloc(length);
memcpy(buffer, data, length);
// plain->isAnimated = false; this is unnecessary since we use calloc
plain->width = width;
plain->height = height;
plain->buffer = buffer;
plain->bufferLen = (int) length;
return plain;
}
void render(IMAGE *image, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
copy_pixels(image->buffer, image->width, image->height, src_x, src_y, dst, dst_w, dst_h, dst_x,
dst_y, width, height);
}
void advance(IMAGE *image) {
if (image->isAnimated) {
AImageDecoder_advanceFrame(image->src->decoder);
if (AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen) == ANDROID_IMAGE_DECODER_FINISHED) {
AImageDecoder_rewind(image->src->decoder);
AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen);
}
}
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecode(JNIEnv *env, jclass clazz, jint fd) {
IMAGE *image = createFromFd(fd);
return (jlong) image;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeCreate(JNIEnv *env, jclass clazz, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels = NULL;
void *image = NULL;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
image = create((int) info.width, (int) info.height, pixels);
AndroidBitmap_unlockPixels(env, bitmap);
return (jlong) image;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jlong ptr, jint src_x,
Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jobject srcBitmap, jint src_x,
jint src_y, jobject dst, jint dst_x, jint dst_y, jint width,
jint height) {
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmapInfo dstInfo;
AndroidBitmapInfo srcInfo;
void *srcPixels = NULL;
void *dstPixels = NULL;
AndroidBitmap_getInfo(env, dst, &info);
AndroidBitmap_lockPixels(env, dst, &pixels);
AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);
AndroidBitmap_lockPixels(env, dst, &dstPixels);
AndroidBitmap_getInfo(env, srcBitmap, &srcInfo);
AndroidBitmap_getInfo(env, dst, &dstInfo);
render((IMAGE *) ptr, src_x, src_y, pixels, (int) info.width, (int) info.height, dst_x, dst_y,
width,
height);
copy_pixels(srcPixels, srcInfo.width, srcInfo.height, src_x, src_y, dstPixels, dstInfo.width,
dstInfo.height, dst_x, dst_y,
width,
height);
AndroidBitmap_unlockPixels(env, dst);
AndroidBitmap_unlockPixels(env, srcBitmap);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr, jboolean init,
jint src_x, jint src_y, jint width, jint height) {
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init,
jint offset_x, jint offset_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
render((IMAGE *) ptr, src_x, src_y, tile_buffer, width, height, 0, 0, width, height);
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_lockPixels(env, bitmap, &pixels);
AndroidBitmap_getInfo(env, bitmap, &info);
copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height);
AndroidBitmap_unlockPixels(env, bitmap);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
@@ -299,44 +164,3 @@ Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr,
tile_buffer);
}
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeAdvance(JNIEnv *env, jclass clazz, jlong ptr) {
advance((IMAGE *) ptr);
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetDelay(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->delay;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_image_Image_nativeIsOpaque(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->alpha;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRecycle(JNIEnv *env, jclass clazz, jlong ptr) {
recycle((IMAGE *) ptr);
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecodeAddr(JNIEnv *env, jclass clazz, jlong addr) {
IMAGE *image = createFromAddr((void *) addr, *(long long *) addr);
return (jlong) image;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetFormat(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->isAnimated;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetWidth(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->width;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetHeight(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->height;
}
@@ -19,6 +19,7 @@ package com.hippo
import android.content.Context
import android.net.Uri
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
@@ -27,7 +28,9 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
}
private external fun openArchive(fd: Int, size: Long): Int
external fun extractToAddr(index: Int): Long
external fun extractToByteBuffer(index: Int): ByteBuffer?
external fun releaseByteBuffer(buffer: ByteBuffer)
external fun extractToFd(index: Int, fd: Int)
external fun getFilename(index: Int): String
external fun needPassword(): Boolean
@@ -1,20 +1,8 @@
package com.hippo.app
import android.content.Context
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
open class BaseDialogBuilder constructor(
context: Context
) : MaterialAlertDialogBuilder(context) {
override fun create(): AlertDialog {
return super.create().apply {
window?.run {
addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes.blurBehindRadius = 32
}
}
}
}
) : MaterialAlertDialogBuilder(context)
@@ -0,0 +1,14 @@
package com.hippo.app
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import com.google.android.material.slider.Slider
class HapticSlider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Slider(context, attrs) {
init {
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
}
}
@@ -231,6 +231,21 @@ public final class DiskLruCache implements Closeable {
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
@@ -247,21 +262,6 @@ public final class DiskLruCache implements Closeable {
}
};
private static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
}
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
@@ -23,7 +23,6 @@ import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
@@ -140,7 +139,7 @@ class StrictLineReader implements Closeable {
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
String res = new String(buf, pos, lineEnd - pos, charset.name());
String res = new String(buf, pos, lineEnd - pos, charset);
pos = i + 1;
return res;
}
@@ -152,11 +151,7 @@ class StrictLineReader implements Closeable {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e); // Since we control the charset this will never happen.
}
return new String(buf, 0, length, charset);
}
};
@@ -25,7 +25,6 @@ import androidx.annotation.UiThread;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.okhttp.ChromeRequestBuilder;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.IOException;
@@ -35,8 +34,6 @@ import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -22,6 +22,7 @@ import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
@@ -29,17 +30,25 @@ import androidx.core.graphics.drawable.DrawableCompat;
public class DrawableWrapper extends Drawable implements Drawable.Callback {
private Drawable mDrawable;
private Rect mBounds;
public DrawableWrapper(Drawable drawable) {
this.setWrappedDrawable(drawable);
}
public void draw(@NonNull Canvas canvas) {
this.mDrawable.draw(canvas);
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
} else {
synchronized (mDrawable) {
mDrawable.setBounds(mBounds);
mDrawable.draw(canvas);
}
}
}
protected void onBoundsChange(Rect bounds) {
this.mDrawable.setBounds(bounds);
mBounds = bounds;
}
public int getChangingConfigurations() {
@@ -16,6 +16,7 @@
package com.hippo.drawable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
@@ -25,7 +26,6 @@ import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.Unikery;
import com.hippo.image.ImageBitmap;
import com.hippo.image.ImageDrawable;
import com.hippo.widget.ObservedTextView;
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
@@ -37,6 +37,8 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
private int mTaskId = Unikery.INVALID_ID;
private String mUrl;
private ImageBitmap imageBitmap;
public UnikeryDrawable(ObservedTextView textView, Conaco<ImageBitmap> conaco) {
mTextView = textView;
mTextView.setOnWindowAttachListener(this);
@@ -62,11 +64,11 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
}
private void clearDrawable() {
Drawable drawable = getDrawable();
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).recycle();
}
setDrawable(null);
if (imageBitmap != null) {
imageBitmap.release();
imageBitmap = null;
}
}
@Override
@@ -123,9 +125,9 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
@Override
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
ImageDrawable drawable;
Drawable drawable;
try {
drawable = new ImageDrawable(value);
drawable = value.getDrawable();
} catch (Exception e) {
Log.d(TAG, "The ImageBitmap is recycled", e);
return false;
@@ -134,7 +136,9 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
clearDrawable();
setDrawable(drawable);
drawable.start();
imageBitmap = value;
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
animatedImageDrawable.start();
return true;
}
@@ -64,8 +64,7 @@ public final class LayoutManagerUtils {
RecyclerView.LayoutManager layoutManager, Context context, int position,
int millisecondsPerInch) {
SimpleSmoothScroller smoothScroller;
if (layoutManager instanceof LinearLayoutManager) {
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (layoutManager instanceof final LinearLayoutManager linearLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -73,8 +72,7 @@ public final class LayoutManagerUtils {
}
};
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
} else if (layoutManager instanceof final StaggeredGridLayoutManager staggeredGridLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -77,14 +77,12 @@ import com.hippo.yorozuya.OSUtils;
import com.hippo.yorozuya.SimpleHandler;
import java.io.File;
import java.io.IOException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
@@ -165,37 +163,29 @@ public class EhApplication extends SceneApplication {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.cookieJar(getEhCookieStore(application))
.cache(getOkHttpCache(application))
.dns(new EhDns(application))
.addNetworkInterceptor(chain -> {
try {
return chain.proceed(chain.request());
} catch (NullPointerException e) {
// crash on meizu devices due to old Android version
// https://github.com/square/okhttp/issues/3301#issuecomment-348415095
throw new IOException(e.getMessage());
}
})
.proxySelector(getEhProxySelector(application));
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
if (Settings.getDF()) {
X509TrustManager trustManager;
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager trustManager1)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
trustManager = trustManager1;
} catch (Exception e) {
e.printStackTrace();
trustManager = new EhX509TrustManager();
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
builder.sslSocketFactory(new EhSSLSocketFactory(), trustManager);
} catch (Exception e) {
e.printStackTrace();
builder.sslSocketFactory(new EhSSLSocketFactory(), new EhX509TrustManager());
}
application.mOkHttpClient = builder.build();
}
return application.mOkHttpClient;
@@ -327,6 +317,7 @@ public class EhApplication extends SceneApplication {
super.onCreate();
Image.initialize(this);
Native.initialize();
GetText.initialize(this);
StatusCodeException.initialize(this);
@@ -492,9 +483,6 @@ public class EhApplication extends SceneApplication {
Log.i(TAG, "Native memory: " + FileUtils.humanReadableByteCount(
Debug.getNativeHeapAllocatedSize(), false));
}
if (DEBUG_PRINT_IMAGE_COUNT) {
Log.i(TAG, "Image count: " + Image.getImageCount());
}
SimpleHandler.getInstance().postDelayed(this, DEBUG_PRINT_INTERVAL);
}
}.run();
+14 -7
View File
@@ -495,13 +495,15 @@ public class EhDB {
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// Downloads
try {
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// Download dirname
try {
@@ -509,13 +511,15 @@ public class EhDB {
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// History
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// QuickSearch
try {
@@ -537,7 +541,8 @@ public class EhDB {
importList.add(quickSearch);
}
importQuickSearch(importList);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// LocalFavorites
try {
@@ -545,7 +550,8 @@ public class EhDB {
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
// Filter
try {
@@ -556,7 +562,8 @@ public class EhDB {
addFilter(filter);
}
}
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
return null;
} catch (Throwable e) {
@@ -36,7 +36,7 @@ public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
try {
isPipe.obtain();
FileInputStream is = (FileInputStream) isPipe.open();
return ImageBitmap.decode(is.getFD());
return ImageBitmap.decode(is);
} catch (OutOfMemoryError | IOException e) {
return null;
} finally {
@@ -62,11 +62,15 @@ public class Settings {
public static final String KEY_DETAIL_SIZE = "detail_size";
public static final String KEY_THUMB_SIZE = "thumb_size";
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY;
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
private static final String KEY_PREVIEW_NUM = "preview_num";
private static final int DEFAULT_PREVIEW_NUM = 60;
/********************
****** Privacy and Security
********************/
@@ -100,16 +104,13 @@ public class Settings {
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_APP_LANGUAGE = "app_language";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_VERSION_CODE = "version_code";
private static final int DEFAULT_VERSION_CODE = 0;
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
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";
@@ -131,8 +132,6 @@ public class Settings {
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 1;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_FIX_THUMB_URL = "fix_thumb_url";
private static final boolean DEFAULT_FIX_THUMB_URL = false;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
@@ -282,11 +281,9 @@ public class Settings {
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
public static boolean LIST_THUMB_SIZE_INITED = false;
private static int LIST_THUMB_SIZE = 40;
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static EhConfig sEhConfig;
@@ -564,8 +561,8 @@ public class Settings {
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
}
public static boolean getFixThumbUrl() {
return getBoolean(KEY_FIX_THUMB_URL, DEFAULT_FIX_THUMB_URL);
public static boolean getShowComments() {
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
}
public static boolean getRequestNews() {
@@ -588,6 +585,10 @@ public class Settings {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
}
public static int getPreviewNum() {
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
}
public static int getDefaultCategories() {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
@@ -979,11 +980,11 @@ public class Settings {
boolean writeFile = false;
String userID = getString(KEY_USER_ID, null);
File file = AppConfig.getFileInExternalAppDir(FILENAME_USER_ID);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeXml = true;
// Get use ID from out sd card file
userID = FileUtils.read(file);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeFile = true;
userID = generateUserID();
}
@@ -1067,6 +1068,7 @@ public class Settings {
public static void putSecurity(boolean value) {
putBoolean(KEY_SECURITY, value);
}
public static int getSecurityDelay() {
return getInt(KEY_SECURITY_DELAY, 0);
}
@@ -1,95 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client;
/*
* Created by Hippo on 2018/3/23.
*/
import android.content.Context;
import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Hosts;
import com.hippo.ehviewer.Settings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Dns;
public class EhDns implements Dns {
private static final Map<String, List<InetAddress>> builtInHosts;
static {
Map<String, List<InetAddress>> map = new HashMap<>();
put(map, "e-hentai.org", "104.20.134.21", "104.20.135.21", "172.67.0.127");
put(map, "exhentai.org", "178.175.128.252", "178.175.129.252", "178.175.129.254", "178.175.128.254",
"178.175.132.20", "178.175.132.22");
put(map, "repo.e-hentai.org", "94.100.28.57", "94.100.29.73");
put(map, "forums.e-hentai.org", "94.100.18.243");
put(map, "ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt0.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt1.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt2.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt3.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "ul.ehgt.org", "94.100.24.82", "94.100.24.72");
put(map, "raw.githubusercontent.com", "151.101.0.133", "151.101.64.133", "151.101.128.133", "151.101.192.133");
builtInHosts = map;
}
private final Hosts hosts;
public EhDns(Context context) {
hosts = EhApplication.getHosts(context);
}
private static void put(Map<String, List<InetAddress>> map, String host, String... ips) {
List<InetAddress> addresses = new ArrayList<>();
for (String ip : ips) {
addresses.add(Hosts.toInetAddress(host, ip));
}
map.put(host, addresses);
}
@NonNull
@Override
public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
List<InetAddress> inetAddresses = hosts.get(hostname);
if (inetAddresses != null) {
return inetAddresses;
}
if (Settings.getBuiltInHosts()) {
inetAddresses = builtInHosts.get(hostname);
if (inetAddresses != null) {
return inetAddresses;
}
}
return SYSTEM.lookup(hostname);
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2018 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.content.Context
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.Hosts
import com.hippo.ehviewer.Settings
import okhttp3.Dns
import java.net.InetAddress
import java.net.UnknownHostException
class EhDns(context: Context) : Dns {
private val hosts = EhApplication.getHosts(context)
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
init {
/* Pair(ip: String!, blockedByCCP: Boolean!) */
val ehgtHosts = arrayOf(
Pair("37.48.89.44", false),
Pair("81.171.10.48", false),
Pair("178.162.139.24", false),
Pair("178.162.140.212", false),
Pair("2001:1af8:4700:a062:8::47de", false),
Pair("2001:1af8:4700:a062:9::47de", true),
Pair("2001:1af8:4700:a0c9:4::47de", false),
Pair("2001:1af8:4700:a0c9:3::47de", true),
)
put(
builtInHosts,
"e-hentai.org",
Pair("104.20.134.21", true),
Pair("104.20.135.21", false),
Pair("172.67.0.127", false),
)
put(
builtInHosts,
"exhentai.org",
Pair("178.175.128.252", false),
Pair("178.175.129.252", false),
Pair("178.175.129.254", false),
Pair("178.175.128.254", false),
Pair("178.175.132.20", false),
Pair("178.175.132.22", false),
)
put(
builtInHosts,
"repo.e-hentai.org",
Pair("94.100.28.57", true),
Pair("94.100.29.73", true),
)
put(
builtInHosts,
"forums.e-hentai.org",
Pair("94.100.18.243", false),
)
put(
builtInHosts,
"ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt0.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt1.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt2.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt3.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"ul.ehgt.org",
Pair("94.100.24.82", true),
Pair("94.100.24.72", true),
)
put(
builtInHosts,
"raw.githubusercontent.com",
Pair("151.101.0.133", false),
Pair("151.101.64.133", false),
Pair("151.101.128.133", false),
Pair("151.101.192.133", false),
)
}
private fun put(
map: MutableMap<String, List<InetAddress>>,
host: String,
vararg ips: Pair<String, Boolean>
) {
map[host] = ips.mapNotNull { pair ->
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.getDF() && pair.second }
}
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
?: Dns.SYSTEM.lookup(hostname)
}
}
@@ -245,12 +245,6 @@ public class EhEngine {
}
}
}
if (Settings.getFixThumbUrl()) {
for (GalleryInfo info : list) {
info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb);
}
}
}
public static GalleryListParser.Result getGalleryList(@Nullable EhClient.Task task, OkHttpClient okHttpClient,
@@ -1,79 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class EhSSLSocketFactory extends SSLSocketFactory {
@Override
public String[] getDefaultCipherSuites() {
return ((SSLSocketFactory) getDefault()).getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return ((SSLSocketFactory) getDefault()).getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault();
if (!Settings.getDF()) {
return defaultFactory.createSocket(s, host, port, autoClose);
}
String address = s.getInetAddress().getHostAddress();
SSLSocket socket = (SSLSocket) defaultFactory.createSocket(s, address, port, autoClose);
SSLSession sslSession = socket.getSession();
Log.d("EhSSLSocketFactory", "Host: " + host + " Address: " + address + " Protocol:" + sslSession.getProtocol() + " CipherSuite:" + sslSession.getCipherSuite());
socket.setEnabledProtocols(socket.getSupportedProtocols());
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return getDefault().createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return getDefault().createSocket(address, port, localAddress, localPort);
}
}
@@ -0,0 +1,72 @@
/*
* 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
import java.net.InetAddress
import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
class EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> {
return sslSocketFactory.defaultCipherSuites
}
override fun getSupportedCipherSuites(): Array<String> {
return sslSocketFactory.supportedCipherSuites
}
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
val sslSession = socket.session
Log.d(
"EhSSLSocketFactory",
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
)
return socket
}
override fun createSocket(host: String, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
host: String,
port: Int,
localHost: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(host, port, localHost, localPort)
}
override fun createSocket(host: InetAddress, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
address: InetAddress,
port: Int,
localAddress: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
}
}
@@ -21,11 +21,6 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.Settings;
import com.hippo.network.UrlBuilder;
import java.util.List;
import java.util.ListIterator;
import okhttp3.HttpUrl;
public class EhUrl {
public static final int SITE_E = 0;
@@ -59,8 +54,6 @@ public class EhUrl {
public static final String ORIGIN_EX = REFERER_EX;
public static final String REFERER_E = "https://" + DOMAIN_E;
public static final String ORIGIN_E = REFERER_E;
private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/";
private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/";
public static String getGalleryDetailUrl(long gid, String token) {
return getGalleryDetailUrl(gid, token, 0, false);
@@ -200,44 +193,4 @@ public class EhUrl {
return URL_WATCHED_EX;
}
}
public static String getThumbUrlPrefix() {
switch (Settings.getGallerySite()) {
default:
//case SITE_E:
return URL_PREFIX_THUMB_E;
//case SITE_EX:
// return URL_PREFIX_THUMB_EX;
}
}
public static String getFixedPreviewThumbUrl(String originUrl) {
HttpUrl url = HttpUrl.parse(originUrl);
if (url == null) return originUrl;
List<String> pathSegments = url.pathSegments();
if (pathSegments.size() < 3) return originUrl;
ListIterator<String> iterator = pathSegments.listIterator(pathSegments.size());
// The last segments, like
// 317a1a254cd9c3269e71b2aa2671fe8d28c91097-260198-640-480-png_250.jpg
if (!iterator.hasPrevious()) return originUrl;
String lastSegment = iterator.previous();
// The second last segments, like
// 7a
if (!iterator.hasPrevious()) return originUrl;
String secondLastSegment = iterator.previous();
// The third last segments, like
// 31
if (!iterator.hasPrevious()) return originUrl;
String thirdLastSegment = iterator.previous();
// Check path segments
if (lastSegment != null && secondLastSegment != null
&& thirdLastSegment != null
&& lastSegment.startsWith(thirdLastSegment)
&& lastSegment.startsWith(secondLastSegment, thirdLastSegment.length())) {
return getThumbUrlPrefix() + thirdLastSegment + "/" + secondLastSegment + "/" + lastSegment;
} else {
return originUrl;
}
}
}
@@ -20,6 +20,7 @@ import android.content.Context;
import android.graphics.Color;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhApplication;
@@ -94,6 +95,7 @@ public class EhUtils {
return CATEGORY_VALUES[i];
}
@NonNull
public static String getCategory(int type) {
int i;
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
@@ -15,30 +15,23 @@
* 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
package com.hippo.ehviewer.client;
import android.annotation.SuppressLint
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
import android.annotation.SuppressLint;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class EhX509TrustManager implements X509TrustManager {
@SuppressLint("CustomX509TrustManager")
class EhX509TrustManager : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
}
@@ -16,16 +16,17 @@
package com.hippo.ehviewer.client.data;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.network.UrlBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FavListUrlBuilder implements Parcelable {
@@ -44,7 +45,9 @@ public class FavListUrlBuilder implements Parcelable {
}
};
private static final String TAG = FavListUrlBuilder.class.getSimpleName();
private String mPrev;
private String mNext;
private String mJumpTo;
private String mKeyword;
private int mFavCat = FAV_CAT_ALL;
@@ -52,7 +55,9 @@ public class FavListUrlBuilder implements Parcelable {
}
protected FavListUrlBuilder(Parcel in) {
this.mPrev = in.readString();
this.mNext = in.readString();
this.mJumpTo = in.readString();
this.mKeyword = in.readString();
this.mFavCat = in.readInt();
}
@@ -61,10 +66,18 @@ public class FavListUrlBuilder implements Parcelable {
return favCat >= 0 && favCat <= 9;
}
public void setPrev(String prev) {
mPrev = prev;
}
public void setNext(String next) {
mNext = next;
}
public void setJumpTo(String jumpTo) {
mJumpTo = jumpTo;
}
public String getKeyword() {
return mKeyword;
}
@@ -93,21 +106,31 @@ public class FavListUrlBuilder implements Parcelable {
ub.addQuery("favcat", "all");
}
if (!TextUtils.isEmpty(mKeyword)) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Can't URLEncoder.encode " + mKeyword);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
} else {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8.displayName()));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
}
if (mPrev != null && !mPrev.isEmpty()) {
ub.addQuery("prev", mPrev);
}
if (mNext != null && !mNext.isEmpty()) {
ub.addQuery("next", mNext);
}
if (mJumpTo != null && !mJumpTo.isEmpty()) {
ub.addQuery("seek", mJumpTo);
}
return ub.build();
}
@@ -118,7 +141,9 @@ public class FavListUrlBuilder implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.mPrev);
dest.writeString(this.mNext);
dest.writeString(this.mJumpTo);
dest.writeString(this.mKeyword);
dest.writeInt(this.mFavCat);
}
@@ -1,643 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.widget.AdvanceSearchTable;
import com.hippo.network.UrlBuilder;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class ListUrlBuilder implements Cloneable, Parcelable {
// Mode
public static final int MODE_NORMAL = 0x0;
public static final int MODE_UPLOADER = 0x1;
public static final int MODE_TAG = 0x2;
public static final int MODE_WHATS_HOT = 0x3;
public static final int MODE_IMAGE_SEARCH = 0x4;
public static final int MODE_SUBSCRIPTION = 0x5;
public static final int MODE_TOPLIST = 0x6;
public static final int DEFAULT_ADVANCE = AdvanceSearchTable.SNAME | AdvanceSearchTable.STAGS;
public static final int DEFAULT_MIN_RATING = 2;
public static final Creator<ListUrlBuilder> CREATOR = new Creator<ListUrlBuilder>() {
@Override
public ListUrlBuilder createFromParcel(Parcel source) {
return new ListUrlBuilder(source);
}
@Override
public ListUrlBuilder[] newArray(int size) {
return new ListUrlBuilder[size];
}
};
@Mode
private int mMode = MODE_NORMAL;
private int mNextGid = 0;
private int mPrevGid = 0;
private int mCategory = EhUtils.NONE;
private String mKeyword = null;
private String mSHash = null;
private int mAdvanceSearch = -1;
private int mMinRating = -1;
private int mPageFrom = -1;
private int mPageTo = -1;
private String mImagePath;
private boolean mUseSimilarityScan;
private boolean mOnlySearchCovers;
private boolean mShowExpunged;
public ListUrlBuilder() {
}
@SuppressWarnings("WrongConstant")
protected ListUrlBuilder(Parcel in) {
this.mMode = in.readInt();
this.mNextGid = in.readInt();
this.mPrevGid = in.readInt();
this.mCategory = in.readInt();
this.mKeyword = in.readString();
this.mAdvanceSearch = in.readInt();
this.mMinRating = in.readInt();
this.mPageFrom = in.readInt();
this.mPageTo = in.readInt();
this.mImagePath = in.readString();
this.mUseSimilarityScan = in.readByte() != 0;
this.mOnlySearchCovers = in.readByte() != 0;
this.mShowExpunged = in.readByte() != 0;
this.mSHash = in.readString();
}
/**
* Make this ListUrlBuilder point to homepage
*/
public void reset() {
mMode = MODE_NORMAL;
mNextGid = 0;
mPrevGid = 0;
mCategory = EhUtils.NONE;
mKeyword = null;
mAdvanceSearch = -1;
mMinRating = -1;
mPageFrom = -1;
mPageTo = -1;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
mSHash = null;
}
@Override
public ListUrlBuilder clone() {
try {
return (ListUrlBuilder) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
@Mode
public int getMode() {
return mMode;
}
public void setMode(@Mode int mode) {
mMode = mode;
}
public void setNextGid(int nextGid) {
mNextGid = nextGid;
}
public void setPrevGid(int prevGid) {
mPrevGid = prevGid;
}
public int getCategory() {
return mCategory;
}
public void setCategory(int category) {
mCategory = category;
}
public String getKeyword() {
return MODE_UPLOADER == mMode ? "uploader:" + mKeyword : mKeyword;
}
public void setKeyword(String keyword) {
mKeyword = keyword;
}
public int getAdvanceSearch() {
return mAdvanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
mAdvanceSearch = advanceSearch;
}
public int getMinRating() {
return mMinRating;
}
public void setMinRating(int minRating) {
mMinRating = minRating;
}
public int getPageFrom() {
return mPageFrom;
}
public void setPageFrom(int pageFrom) {
mPageFrom = pageFrom;
}
public int getPageTo() {
return mPageTo;
}
public void setPageTo(int pageTo) {
mPageTo = pageTo;
}
@Nullable
public String getImagePath() {
return mImagePath;
}
public void setImagePath(String imagePath) {
mImagePath = imagePath;
}
public boolean isUseSimilarityScan() {
return mUseSimilarityScan;
}
public void setUseSimilarityScan(boolean useSimilarityScan) {
mUseSimilarityScan = useSimilarityScan;
}
public boolean isOnlySearchCovers() {
return mOnlySearchCovers;
}
public void setOnlySearchCovers(boolean onlySearchCovers) {
mOnlySearchCovers = onlySearchCovers;
}
public boolean isShowExpunged() {
return mShowExpunged;
}
public void setShowExpunged(boolean showExpunged) {
mShowExpunged = showExpunged;
}
/**
* Make them the same
*
* @param lub The template
*/
public void set(ListUrlBuilder lub) {
mMode = lub.mMode;
mNextGid = lub.mNextGid;
mPrevGid = lub.mPrevGid;
mCategory = lub.mCategory;
mKeyword = lub.mKeyword;
mAdvanceSearch = lub.mAdvanceSearch;
mMinRating = lub.mMinRating;
mPageFrom = lub.mPageFrom;
mPageTo = lub.mPageTo;
mImagePath = lub.mImagePath;
mUseSimilarityScan = lub.mUseSimilarityScan;
mOnlySearchCovers = lub.mOnlySearchCovers;
mShowExpunged = lub.mShowExpunged;
mSHash = lub.mSHash;
}
public void set(QuickSearch q) {
mMode = q.mode;
mCategory = q.category;
mKeyword = q.keyword;
mAdvanceSearch = q.advanceSearch;
mMinRating = q.minRating;
mPageFrom = q.pageFrom;
mPageTo = q.pageTo;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
}
public QuickSearch toQuickSearch() {
QuickSearch q = new QuickSearch();
q.mode = mMode;
q.category = mCategory;
q.keyword = mKeyword;
q.advanceSearch = mAdvanceSearch;
q.minRating = mMinRating;
q.pageFrom = mPageFrom;
q.pageTo = mPageTo;
return q;
}
public boolean equalsQuickSearch(QuickSearch q) {
if (null == q) {
return false;
}
if (q.mode != mMode) {
return false;
}
if (q.category != mCategory) {
return false;
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false;
}
if (q.advanceSearch != mAdvanceSearch) {
return false;
}
if (q.minRating != mMinRating) {
return false;
}
if (q.pageFrom != mPageFrom) {
return false;
}
return q.pageTo == mPageTo;
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
public void setQuery(String query) {
reset();
if (TextUtils.isEmpty(query)) {
return;
}
String[] querys = StringUtils.split(query, '&');
int category = 0;
String keyword = null;
boolean enableAdvanceSearch = false;
int advanceSearch = 0;
boolean enableMinRating = false;
int minRating = -1;
boolean enablePage = false;
int pageFrom = -1;
int pageTo = -1;
for (String str : querys) {
int index = str.indexOf('=');
if (index < 0) {
continue;
}
String key = str.substring(0, index);
String value = str.substring(index + 1);
switch (key) {
case "f_cats":
int cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY);
category |= (~cats) & EhConfig.ALL_CATEGORY;
break;
case "f_doujinshi":
if ("1".equals(value)) {
category |= EhConfig.DOUJINSHI;
}
break;
case "f_manga":
if ("1".equals(value)) {
category |= EhConfig.MANGA;
}
break;
case "f_artistcg":
if ("1".equals(value)) {
category |= EhConfig.ARTIST_CG;
}
break;
case "f_gamecg":
if ("1".equals(value)) {
category |= EhConfig.GAME_CG;
}
break;
case "f_western":
if ("1".equals(value)) {
category |= EhConfig.WESTERN;
}
break;
case "f_non-h":
if ("1".equals(value)) {
category |= EhConfig.NON_H;
}
break;
case "f_imageset":
if ("1".equals(value)) {
category |= EhConfig.IMAGE_SET;
}
break;
case "f_cosplay":
if ("1".equals(value)) {
category |= EhConfig.COSPLAY;
}
break;
case "f_asianporn":
if ("1".equals(value)) {
category |= EhConfig.ASIAN_PORN;
}
break;
case "f_misc":
if ("1".equals(value)) {
category |= EhConfig.MISC;
}
break;
case "f_search":
try {
keyword = URLDecoder.decode(value, "utf-8");
} catch (UnsupportedEncodingException | IllegalArgumentException e) {
// Ignore
}
break;
case "advsearch":
if ("1".equals(value)) {
enableAdvanceSearch = true;
}
break;
case "f_sname":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SNAME;
}
break;
case "f_stags":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STAGS;
}
break;
case "f_sdesc":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDESC;
}
break;
case "f_storr":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STORR;
}
break;
case "f_sto":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STO;
}
break;
case "f_sdt1":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT1;
}
break;
case "f_sdt2":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT2;
}
break;
case "f_sh":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SH;
}
break;
case "f_sfl":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFL;
}
break;
case "f_sfu":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFU;
}
break;
case "f_sft":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFT;
}
break;
case "f_sr":
if ("on".equals(value)) {
enableMinRating = true;
}
break;
case "f_srdd":
minRating = NumberUtils.parseIntSafely(value, -1);
break;
case "f_sp":
if ("on".equals(value)) {
enablePage = true;
}
break;
case "f_spf":
pageFrom = NumberUtils.parseIntSafely(value, -1);
break;
case "f_spt":
pageTo = NumberUtils.parseIntSafely(value, -1);
break;
case "f_shash":
mSHash = value;
break;
}
}
mCategory = category;
mKeyword = keyword;
if (enableAdvanceSearch) {
mAdvanceSearch = advanceSearch;
if (enableMinRating) {
mMinRating = minRating;
} else {
mMinRating = -1;
}
if (enablePage) {
mPageFrom = pageFrom;
mPageTo = pageTo;
} else {
mPageFrom = -1;
mPageTo = -1;
}
} else {
mAdvanceSearch = -1;
}
}
public String build() {
switch (mMode) {
default:
case MODE_NORMAL:
case MODE_SUBSCRIPTION: {
String url;
if (mMode == MODE_NORMAL) {
url = EhUrl.getHost();
} else {
url = EhUrl.getWatchedUrl();
}
UrlBuilder ub = new UrlBuilder(url);
if (mCategory != EhUtils.NONE) {
ub.addQuery("f_cats", (~mCategory) & EhConfig.ALL_CATEGORY);
}
// Search key
if (mKeyword != null) {
String keyword = mKeyword.trim();
if (!keyword.isEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash);
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid);
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid);
}
// Advance search
if (mAdvanceSearch != -1) {
ub.addQuery("advsearch", "1");
if ((mAdvanceSearch & AdvanceSearchTable.SNAME) != 0)
ub.addQuery("f_sname", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STAGS) != 0)
ub.addQuery("f_stags", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDESC) != 0)
ub.addQuery("f_sdesc", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STORR) != 0)
ub.addQuery("f_storr", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STO) != 0) ub.addQuery("f_sto", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT1) != 0)
ub.addQuery("f_sdt1", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT2) != 0)
ub.addQuery("f_sdt2", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SH) != 0) ub.addQuery("f_sh", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFL) != 0) ub.addQuery("f_sfl", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFU) != 0) ub.addQuery("f_sfu", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFT) != 0) ub.addQuery("f_sft", "on");
// Set min star
if (mMinRating != -1) {
ub.addQuery("f_sr", "on");
ub.addQuery("f_srdd", mMinRating);
}
// Pages
if (mPageFrom != -1 || mPageTo != -1) {
ub.addQuery("f_sp", "on");
ub.addQuery("f_spf", mPageFrom != -1 ? Integer.toString(mPageFrom) : "");
ub.addQuery("f_spt", mPageTo != -1 ? Integer.toString(mPageTo) : "");
}
}
return ub.build();
}
case MODE_UPLOADER: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("uploader/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid);
}
return sb.toString();
}
case MODE_TAG: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("tag/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid);
}
return sb.toString();
}
case MODE_WHATS_HOT:
return EhUrl.getPopularUrl();
case MODE_IMAGE_SEARCH:
return EhUrl.getImageSearchUrl();
case MODE_TOPLIST:
StringBuilder sb = new StringBuilder(EhUrl.HOST_E);
sb.append("toplist.php?tl=");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mNextGid != 0) {
sb.append("&p=").append(mNextGid);
}
return sb.toString();
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode);
dest.writeInt(this.mNextGid);
dest.writeInt(this.mPrevGid);
dest.writeInt(this.mCategory);
dest.writeString(this.mKeyword);
dest.writeInt(this.mAdvanceSearch);
dest.writeInt(this.mMinRating);
dest.writeInt(this.mPageFrom);
dest.writeInt(this.mPageTo);
dest.writeString(this.mImagePath);
dest.writeByte(mUseSimilarityScan ? (byte) 1 : (byte) 0);
dest.writeByte(mOnlySearchCovers ? (byte) 1 : (byte) 0);
dest.writeByte(mShowExpunged ? (byte) 1 : (byte) 0);
dest.writeString(this.mSHash);
}
@IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH, MODE_SUBSCRIPTION, MODE_TOPLIST})
@Retention(RetentionPolicy.SOURCE)
private @interface Mode {
}
}
@@ -0,0 +1,547 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhConfig
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.widget.AdvanceSearchTable
import com.hippo.network.UrlBuilder
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
open class ListUrlBuilder : Cloneable, Parcelable {
@get:Mode
@Mode
var mode = MODE_NORMAL
private var mPrevGid = 0
private var mNextGid = 0
private var mJumpTo: String? = null
var category = EhUtils.NONE
private var mKeyword: String? = null
private var mSHash: String? = null
var advanceSearch = -1
var minRating = -1
var pageFrom = -1
var pageTo = -1
var imagePath: String? = null
var isUseSimilarityScan = false
var isOnlySearchCovers = false
var isShowExpunged = false
constructor()
protected constructor(parcel: Parcel) {
mode = parcel.readInt()
mPrevGid = parcel.readInt()
mNextGid = parcel.readInt()
mJumpTo = parcel.readString()
this.category = parcel.readInt()
mKeyword = parcel.readString()
advanceSearch = parcel.readInt()
minRating = parcel.readInt()
pageFrom = parcel.readInt()
pageTo = parcel.readInt()
imagePath = parcel.readString()
isUseSimilarityScan = parcel.readByte().toInt() != 0
isOnlySearchCovers = parcel.readByte().toInt() != 0
isShowExpunged = parcel.readByte().toInt() != 0
mSHash = parcel.readString()
}
fun reset() {
mode = MODE_NORMAL
mPrevGid = 0
mNextGid = 0
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
mSHash = null
}
public override fun clone(): ListUrlBuilder {
return try {
super.clone() as ListUrlBuilder
} catch (e: CloneNotSupportedException) {
throw IllegalStateException(e)
}
}
fun setPrevGid(prevGid: Int) {
mPrevGid = prevGid
}
fun setNextGid(nextGid: Int) {
mNextGid = nextGid
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
/**
* Make them the same
*
* @param lub The template
*/
fun set(lub: ListUrlBuilder) {
mode = lub.mode
mPrevGid = lub.mPrevGid
mNextGid = lub.mNextGid
mJumpTo = lub.mJumpTo
this.category = lub.category
mKeyword = lub.mKeyword
advanceSearch = lub.advanceSearch
minRating = lub.minRating
pageFrom = lub.pageFrom
pageTo = lub.pageTo
imagePath = lub.imagePath
isUseSimilarityScan = lub.isUseSimilarityScan
isOnlySearchCovers = lub.isOnlySearchCovers
isShowExpunged = lub.isShowExpunged
mSHash = lub.mSHash
}
fun set(q: QuickSearch) {
mode = q.mode
this.category = q.category
mKeyword = q.keyword
advanceSearch = q.advanceSearch
minRating = q.minRating
pageFrom = q.pageFrom
pageTo = q.pageTo
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
}
fun toQuickSearch(): QuickSearch {
val q = QuickSearch()
q.mode = mode
q.category = this.category
q.keyword = mKeyword
q.advanceSearch = advanceSearch
q.minRating = minRating
q.pageFrom = pageFrom
q.pageTo = pageTo
return q
}
fun equalsQuickSearch(q: QuickSearch?): Boolean {
if (null == q) {
return false
}
if (q.mode != mode) {
return false
}
if (q.category != this.category) {
return false
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false
}
if (q.advanceSearch != advanceSearch) {
return false
}
if (q.minRating != minRating) {
return false
}
return if (q.pageFrom != pageFrom) {
false
} else q.pageTo == pageTo
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
fun setQuery(query: String?) {
reset()
if (TextUtils.isEmpty(query)) {
return
}
val querys = StringUtils.split(query, '&')
var category = 0
var keyword: String? = null
var enableAdvanceSearch = false
var advanceSearch = 0
var enableMinRating = false
var minRating = -1
var enablePage = false
var pageFrom = -1
var pageTo = -1
for (str in querys) {
val index = str.indexOf('=')
if (index < 0) {
continue
}
val key = str.substring(0, index)
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY)
category = category or (cats.inv() and EhConfig.ALL_CATEGORY)
}
"f_doujinshi" -> if ("1" == value) {
category = category or EhConfig.DOUJINSHI
}
"f_manga" -> if ("1" == value) {
category = category or EhConfig.MANGA
}
"f_artistcg" -> if ("1" == value) {
category = category or EhConfig.ARTIST_CG
}
"f_gamecg" -> if ("1" == value) {
category = category or EhConfig.GAME_CG
}
"f_western" -> if ("1" == value) {
category = category or EhConfig.WESTERN
}
"f_non-h" -> if ("1" == value) {
category = category or EhConfig.NON_H
}
"f_imageset" -> if ("1" == value) {
category = category or EhConfig.IMAGE_SET
}
"f_cosplay" -> if ("1" == value) {
category = category or EhConfig.COSPLAY
}
"f_asianporn" -> if ("1" == value) {
category = category or EhConfig.ASIAN_PORN
}
"f_misc" -> if ("1" == value) {
category = category or EhConfig.MISC
}
"f_search" -> try {
keyword = URLDecoder.decode(value, "utf-8")
} catch (e: UnsupportedEncodingException) {
// Ignore
} catch (_: IllegalArgumentException) {
}
"advsearch" -> if ("1" == value) {
enableAdvanceSearch = true
}
"f_sh" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SH
}
"f_sto" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.STO
}
"f_sfl" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFL
}
"f_sfu" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFU
}
"f_sft" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFT
}
"f_sr" -> if ("on" == value) {
enableMinRating = true
}
"f_srdd" -> minRating = NumberUtils.parseIntSafely(value, -1)
"f_sp" -> if ("on" == value) {
enablePage = true
}
"f_spf" -> pageFrom = NumberUtils.parseIntSafely(value, -1)
"f_spt" -> pageTo = NumberUtils.parseIntSafely(value, -1)
"f_shash" -> mSHash = value
}
}
this.category = category
mKeyword = keyword
if (enableAdvanceSearch) {
this.advanceSearch = advanceSearch
if (enableMinRating) {
this.minRating = minRating
} else {
this.minRating = -1
}
if (enablePage) {
this.pageFrom = pageFrom
this.pageTo = pageTo
} else {
this.pageFrom = -1
this.pageTo = -1
}
} else {
this.advanceSearch = -1
}
}
fun build(): String {
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
// Search key
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
mJumpTo?.let {
ub.addQuery("seek", mJumpTo!!)
mPrevGid = 0
mNextGid = 0
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
// Set min star
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
// Pages
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("uploader/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
MODE_TAG -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("tag/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
MODE_TOPLIST -> {
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mNextGid != 0) {
sb.append("&p=").append(mNextGid)
}
sb.toString()
}
else -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (_: UnsupportedEncodingException) {
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
}
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(mode)
dest.writeInt(mPrevGid)
dest.writeInt(mNextGid)
dest.writeString(mJumpTo)
dest.writeInt(this.category)
dest.writeString(mKeyword)
dest.writeInt(advanceSearch)
dest.writeInt(minRating)
dest.writeInt(pageFrom)
dest.writeInt(pageTo)
dest.writeString(imagePath)
dest.writeByte(if (isUseSimilarityScan) 1.toByte() else 0.toByte())
dest.writeByte(if (isOnlySearchCovers) 1.toByte() else 0.toByte())
dest.writeByte(if (isShowExpunged) 1.toByte() else 0.toByte())
dest.writeString(mSHash)
}
@IntDef(
MODE_NORMAL,
MODE_UPLOADER,
MODE_TAG,
MODE_WHATS_HOT,
MODE_IMAGE_SEARCH,
MODE_SUBSCRIPTION,
MODE_TOPLIST
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Mode
companion object {
const val MODE_NORMAL = 0x0
const val MODE_UPLOADER = 0x1
const val MODE_TAG = 0x2
const val MODE_WHATS_HOT = 0x3
const val MODE_IMAGE_SEARCH = 0x4
const val MODE_SUBSCRIPTION = 0x5
const val MODE_TOPLIST = 0x6
@JvmField
val CREATOR: Creator<ListUrlBuilder> = object : Creator<ListUrlBuilder> {
override fun createFromParcel(source: Parcel): ListUrlBuilder {
return ListUrlBuilder(source)
}
override fun newArray(size: Int): Array<ListUrlBuilder?> {
return arrayOfNulls(size)
}
}
}
}
@@ -31,20 +31,22 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FavoritesParser {
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+(-\\d+)?)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+(-\\d+)?)");
public static Result parse(String body) throws Exception {
if (body.contains("This page requires you to log on.</p>")) {
throw new EhException(GetText.getString(R.string.need_sign_in));
}
Document d = Jsoup.parse(body);
String[] catArray = new String[10];
int[] countArray = new int[10];
try {
Document d = Jsoup.parse(body);
Element ido = JsoupUtils.getElementByClass(d, "ido");
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
@@ -61,17 +63,17 @@ public class FavoritesParser {
e.printStackTrace();
throw new ParseException("Parse favorites error", body);
}
Result re = new Result();
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);
}
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find()) re.prevPage = matcherPrev.group(1);
if (matcherNext.find()) re.nextPage = matcherNext.group(1);
} catch (Throwable e) {
e.printStackTrace();
}
@@ -88,6 +90,7 @@ public class FavoritesParser {
public static class Result {
public String[] catArray; // Size 10
public int[] countArray; // Size 10
public String prevPage;
public String nextPage;
public List<GalleryInfo> galleryInfoList;
}
@@ -19,8 +19,6 @@ package com.hippo.ehviewer.client.parser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
@@ -48,14 +46,12 @@ import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -66,9 +62,6 @@ public class GalleryDetailParser {
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
private static final Pattern PATTERN_TAG_GROUP = Pattern.compile("<tr><td[^<>]+>([\\w\\s]+):</td><td>(?:<div[^<>]+><a[^<>]+>[\\w\\s]+</a></div>)+</td></tr>");
private static final Pattern PATTERN_TAG = Pattern.compile("<div[^<>]+><a[^<>]+>([\\w\\s]+)</a></div>");
private static final Pattern PATTERN_COMMENT = Pattern.compile("<div class=\"c3\">Posted on ([^<>]+) by: &nbsp; <a[^<>]+>([^<>]+)</a>.+?<div class=\"c6\"[^>]*>(.+?)</div><div class=\"c[78]\"");
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
@@ -78,16 +71,13 @@ public class GalleryDetailParser {
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
private static final DateFormat WEB_COMMENT_DATE_FORMAT = new SimpleDateFormat("dd MMMMM yyyy, HH:mm", Locale.US);
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
static {
WEB_COMMENT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public static GalleryDetail parse(String body) throws EhException {
if (body.contains(OFFENSIVE_STRING)) {
throw new OffensiveException();
@@ -198,14 +188,13 @@ public class GalleryDetailParser {
gd.posted = "";
gd.parent = "";
gd.visible = "";
gd.visible = "";
gd.size = "";
gd.pages = 0;
gd.favoriteCount = 0;
try {
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
parseDetailInfo(gd, es.get(i), body);
parseDetailInfo(gd, es.get(i));
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -293,7 +282,7 @@ public class GalleryDetailParser {
}
}
private static void parseDetailInfo(GalleryDetail gd, Element e, String body) {
private static void parseDetailInfo(GalleryDetail gd, Element e) {
Elements es = e.children();
if (es.size() < 2) {
return;
@@ -323,20 +312,16 @@ public class GalleryDetailParser {
}
} else if (key.startsWith("Favorited")) {
switch (value) {
case "Never":
gd.favoriteCount = 0;
break;
case "Once":
gd.favoriteCount = 1;
break;
default:
case "Never" -> gd.favoriteCount = 0;
case "Once" -> gd.favoriteCount = 1;
default -> {
int index = value.indexOf(' ');
if (index == -1) {
gd.favoriteCount = 0;
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
}
break;
}
}
}
}
@@ -377,6 +362,7 @@ public class GalleryDetailParser {
public static GalleryTagGroup[] parseTagGroups(Document document) {
try {
Element taglist = document.getElementById("taglist");
assert taglist != null;
Elements tagGroups = taglist.child(0).child(0).children();
return parseTagGroups(tagGroups);
} catch (Throwable e) {
@@ -396,7 +382,7 @@ public class GalleryDetailParser {
list.add(group);
}
}
return list.toArray(new GalleryTagGroup[list.size()]);
return list.toArray(new GalleryTagGroup[0]);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -404,31 +390,6 @@ public class GalleryDetailParser {
}
}
/**
* Parse tag groups with regular expressions
*/
@NonNull
private static GalleryTagGroup[] parseTagGroups(String body) throws EhException {
List<GalleryTagGroup> list = new LinkedList<>();
Matcher m = PATTERN_TAG_GROUP.matcher(body);
while (m.find()) {
GalleryTagGroup tagGroup = new GalleryTagGroup();
tagGroup.groupName = ParserUtils.trim(m.group(1));
parseGroup(tagGroup, m.group(0));
list.add(tagGroup);
}
return list.toArray(new GalleryTagGroup[list.size()]);
}
private static void parseGroup(GalleryTagGroup tagGroup, String body) {
Matcher m = PATTERN_TAG.matcher(body);
while (m.find()) {
tagGroup.addTag(ParserUtils.trim(m.group(1)));
}
}
@Nullable
@SuppressWarnings("ConstantConditions")
public static GalleryComment parseComment(Element element) {
@@ -446,17 +407,15 @@ public class GalleryDetailParser {
}
for (Element e : c4.children()) {
switch (e.text()) {
case "Vote+":
case "Vote+" -> {
comment.voteUpAble = true;
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Vote-":
}
case "Vote-" -> {
comment.voteDownAble = true;
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Edit":
comment.editable = true;
break;
}
case "Edit" -> comment.editable = true;
}
}
}
@@ -477,7 +436,7 @@ public class GalleryDetailParser {
Element c3 = JsoupUtils.getElementByClass(element, "c3");
String temp = c3.ownText();
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
comment.time = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
// user
comment.user = c3.child(0).text();
// comment
@@ -487,7 +446,7 @@ public class GalleryDetailParser {
if (c8 != null) {
Element e = c8.children().first();
if (e != null) {
comment.lastEdited = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
}
}
return comment;
@@ -505,6 +464,7 @@ public class GalleryDetailParser {
public static GalleryCommentList parseComments(Document document) {
try {
Element cdiv = document.getElementById("cdiv");
assert cdiv != null;
Elements c1s = cdiv.getElementsByClass("c1");
List<GalleryComment> list = new ArrayList<>(c1s.size());
@@ -517,20 +477,21 @@ public class GalleryDetailParser {
Element chd = cdiv.getElementById("chd");
MutableBoolean hasMore = new MutableBoolean(false);
assert chd != null;
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int depth) {
public void head(@NonNull Node node, int depth) {
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
hasMore.value = true;
}
}
@Override
public void tail(Node node, int depth) {
public void tail(@NonNull Node node, int depth) {
}
}, chd);
return new GalleryCommentList(list.toArray(new GalleryComment[list.size()]), hasMore.value);
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -538,38 +499,14 @@ public class GalleryDetailParser {
}
}
/**
* Parse comments with regular expressions
*/
@NonNull
public static GalleryComment[] parseComments(String body) {
List<GalleryComment> list = new LinkedList<>();
Matcher m = PATTERN_COMMENT.matcher(body);
while (m.find()) {
String webDateString = ParserUtils.trim(m.group(1));
Date date;
try {
date = WEB_COMMENT_DATE_FORMAT.parse(webDateString);
} catch (java.text.ParseException e) {
date = new Date(0L);
}
GalleryComment comment = new GalleryComment();
comment.time = date.getTime();
comment.user = ParserUtils.trim(m.group(2));
comment.comment = m.group(3);
list.add(comment);
}
return list.toArray(new GalleryComment[list.size()]);
}
/**
* Parse preview pages with html parser
*/
public static int parsePreviewPages(Document document, String body) throws ParseException {
try {
Elements elements = document.getElementsByClass("ptt").first().child(0).child(0).children();
Element ptt = document.getElementsByClass("ptt").first();
assert ptt != null;
Elements elements = ptt.child(0).child(0).children();
return Integer.parseInt(elements.get(elements.size() - 2).text());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -636,9 +573,10 @@ public class GalleryDetailParser {
try {
LargePreviewSet largePreviewSet = new LargePreviewSet();
Element gdt = d.getElementById("gdt");
assert gdt != null;
Elements gdtls = gdt.getElementsByClass("gdtl");
int n = gdtls.size();
if (n <= 0) {
if (n == 0) {
throw new ParseException("Can't parse large preview", body);
}
for (int i = 0; i < n; i++) {
@@ -646,9 +584,6 @@ public class GalleryDetailParser {
String pageUrl = element.attr("href");
element = element.child(0);
String imageUrl = element.attr("src");
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
int index = Integer.parseInt(element.attr("alt")) - 1;
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -674,9 +609,6 @@ public class GalleryDetailParser {
}
String imageUrl = ParserUtils.trim(m.group(3));
String pageUrl = ParserUtils.trim(m.group(1));
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -51,8 +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_GETINT = Pattern.compile("[^0-9]");
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+)");
private static final String[][] FAVORITE_SLOT_RGB = new String[][]{
new String[]{"0", "0", "0"},
@@ -298,21 +298,35 @@ 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);
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 {
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find())
result.prevGid = NumberUtils.parseIntSafely(matcherPrev.group(1), 0);
if (matcherNext.find())
result.nextGid = NumberUtils.parseIntSafely(matcherNext.group(1), 0);
result.founds = Integer.MAX_VALUE;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
result.noWatchedTags = body.contains("<p>You do not have any watched tags");
if (body.contains("No hits found</p>")) {
result.founds = 0;
//noinspection unchecked
result.galleryInfoList = Collections.EMPTY_LIST;
return result;
} else if (d.getElementsByClass("ptt").isEmpty()) {
result.founds = 1;
} else {
result.founds = Integer.MAX_VALUE;
}
}
try {
@@ -333,7 +347,7 @@ public class GalleryListParser {
}
if (list.isEmpty()) {
if (es.size() < 2 || !NO_UNFILTERED_TEXT.equals(es.get(1).text())) {
throw new ParseException("No gallery", body);
Log.d(TAG, "No gallery found");
}
}
result.galleryInfoList = list;
@@ -348,6 +362,8 @@ public class GalleryListParser {
public static class Result {
public int founds;
public int prevGid;
public int nextGid;
public boolean noWatchedTags;
public List<GalleryInfo> galleryInfoList;
}
@@ -17,6 +17,7 @@
package com.hippo.ehviewer.client.parser;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import com.hippo.ehviewer.client.EhUrl;
@@ -27,6 +28,7 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public final class GalleryListUrlParser {
@@ -91,10 +93,15 @@ public final class GalleryListUrlParser {
uploader = path.substring(prefixLength, index);
}
try {
uploader = URLDecoder.decode(uploader, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8);
} else {
try {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(uploader)) {
@@ -120,10 +127,15 @@ public final class GalleryListUrlParser {
tag = path.substring(prefixLength, index);
}
try {
tag = URLDecoder.decode(tag, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8);
} else {
try {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(tag)) {
@@ -19,17 +19,18 @@ package com.hippo.ehviewer.client.parser;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class ParserUtils {
public static final DateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
public static final DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC);
public static synchronized String formatDate(long time) {
return sDateFormat.format(new Date(time));
return formatter.format(Instant.ofEpochMilli(time));
}
public static String trim(String str) {
@@ -48,7 +49,4 @@ public class ParserUtils {
return NumberUtils.parseLongSafely(trim(str).replace(",", ""), defValue);
}
public static float parseFloat(String str, float defValue) {
return NumberUtils.parseFloatSafely(trim(str).replace(",", ""), defValue);
}
}
@@ -4,5 +4,6 @@ import java.util.List;
public interface BasicDao<T> {
List<T> list();
long insert(T t);
}
@@ -58,10 +58,9 @@ public class Filter {
@Override
public boolean equals(Object o) {
if (!(o instanceof Filter)) {
if (!(o instanceof Filter filter)) {
return false;
}
Filter filter = (Filter) o;
return filter.mode == mode && ObjectUtils.equal(filter.text, text);
}
}
@@ -498,6 +498,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
}
mNotifyManager.notify(mId, mBuilder.build())
}
OPS_CANCEL -> mNotifyManager.cancel(mId)
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
}
@@ -17,6 +17,7 @@
package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.graphics.ImageDecoder;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
@@ -37,6 +38,7 @@ import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -51,15 +53,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, Long> streams = new LinkedHashMap<>();
private Thread archiveThread;
private Thread[] decodeThread = new Thread[] {
private final LinkedHashMap<Integer, ByteBuffer> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private Thread archiveThread;
private volatile int size = STATE_WAIT;
private String error;
@@ -161,7 +162,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
int fd;
FileOutputStream stream;
try {
stream = (FileOutputStream)file.openOutputStream();
stream = (FileOutputStream) file.openOutputStream();
fd = Native.getFd(stream.getFD());
} catch (IOException e) {
e.printStackTrace();
@@ -187,7 +188,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[] {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
@@ -298,10 +299,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
long addr = archiveAccessor.extractToAddr(index);
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
synchronized (streams) {
streams.put(index, addr);
streams.put(index, buffer);
streams.notify();
}
}
@@ -313,7 +314,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Long addr;
ByteBuffer buffer;
synchronized (streams) {
if (streams.isEmpty()) {
try {
@@ -324,14 +325,25 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
continue;
}
Iterator<Map.Entry<Integer, Long>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Long> entry = iterator.next();
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, ByteBuffer> entry = iterator.next();
iterator.remove();
index = entry.getKey();
addr = entry.getValue();
buffer = entry.getValue();
}
Image image = Image.decodeAddr(addr, true);
Image image = null;
if (buffer != null) {
try {
image = Image.decode(buffer, false, () -> {
archiveAccessor.releaseByteBuffer(buffer);
return null;
});
} catch (ImageDecoder.DecodeException e) {
archiveAccessor.releaseByteBuffer(buffer);
e.printStackTrace();
}
}
if (image != null) {
notifyPageSucceed(index, image);
} else {
@@ -259,13 +259,9 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable {
InputStream is = null;
try {
is = files[index].openInputStream();
Image image = Image.decode(((FileInputStream) is).getFD(), true);
Image image = Image.decode((FileInputStream) is, false);
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
notifyPageSucceed(index, image);
} catch (IOException e) {
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_reading_failed));
@@ -15,7 +15,8 @@
*/
package com.hippo.ehviewer.preference
import android.content.*
import android.content.Context
import android.content.DialogInterface
import android.text.Html
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
@@ -28,10 +29,9 @@ import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.preference.MessagePreference
import com.hippo.util.addTextToClipboard
import com.hippo.util.getClipboardManager
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.*
import java.util.LinkedList
class AccountPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
@@ -81,9 +81,7 @@ class AccountPreference @JvmOverloads constructor(
super.onPrepareDialogBuilder(builder)
if (message != null) {
builder.setNeutralButton(R.string.settings_eh_identity_cookies_copy) { dialog: DialogInterface?, which: Int ->
context.getClipboardManager().addTextToClipboard(message, true) {
mActivity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)
}
mActivity.addTextToClipboard(message, true)
this@AccountPreference.onClick(dialog, which)
}
}
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.util.WebViewUtil;
import okhttp3.HttpUrl;
@@ -59,10 +58,6 @@ public class SignInRequiredPreference extends Preference implements Preference.O
@Override
public boolean onPreferenceClick(Preference preference) {
if (!WebViewUtil.available(getContext())) {
mActivity.showTip(R.string.error_no_webview, BaseScene.LENGTH_SHORT);
return true;
}
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
HttpUrl e = HttpUrl.get(EhUrl.HOST_E);
HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX);
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.spider;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.AsyncTask;
import android.os.Process;
import android.text.TextUtils;
@@ -1678,8 +1679,8 @@ public final class SpiderQueen implements Runnable {
if (is != null) {
try {
image = Image.decode(((FileInputStream) is).getFD(), true);
} catch (IOException e) {
image = Image.decode(((FileInputStream) is), false);
} catch (ImageDecoder.DecodeException e) {
e.printStackTrace();
} finally {
try {
@@ -47,10 +47,6 @@ import java.util.Objects;
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) {
@@ -22,7 +22,6 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
@@ -71,23 +70,15 @@ abstract class EhActivity : AppCompatActivity() {
WindowInsetsHelper.LISTENER
)
super.onCreate(savedInstanceState)
window.statusBarColor = Color.TRANSPARENT
window.decorView.post {
val rootWindowInsets = window.decorView.rootWindowInsets
if (rootWindowInsets != null && rootWindowInsets.systemWindowInsetBottom >= Resources.getSystem().displayMetrics.density * 40) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
}
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
window.navigationBarColor =
theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
} else {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = true
}
}
}
(application as EhApplication).registerActivity(this)
@@ -303,16 +303,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
break;
}
super.attachBaseContext(newBase);
} 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
@SuppressWarnings({"WrongConstant"})
@@ -404,26 +395,25 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
if (mGalleryView == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL:
float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300;
if (scroll < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
} else {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
if (event.getAction() == MotionEvent.ACTION_SCROLL) {
float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300;
if (scroll < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
} else {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
}
}
return false;
});
@@ -521,7 +511,16 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
mSeekBar = null;
SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable);
}
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result && mSavingPage != -1) {
saveImage(mSavingPage);
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
}
mSavingPage = -1;
});
@Override
protected void onPause() {
@@ -0,0 +1,187 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.util.addTextToClipboard
class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment() {
private var mKeys: ArrayList<String> = arrayListOf()
private var mValues: ArrayList<String> = arrayListOf()
private var mRecyclerView: RecyclerView? = null
private val mDetail: GalleryDetail = detail
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
parseDetail()
val view = inflater.inflate(R.layout.scene_gallery_info, container, false)
val recyclerView = view.findViewById(R.id.recycler_view) as RecyclerView
val adapter = InfoAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.clipToPadding = false
recyclerView.setHasFixedSize(true)
mRecyclerView = recyclerView
return view
}
private fun parseDetail() {
mKeys.add(getString(R.string.header_key))
mValues.add(getString(R.string.header_value))
mKeys.add(getString(R.string.key_gid))
mValues.add(mDetail.gid.toString())
mKeys.add(getString(R.string.key_token))
mValues.add(mDetail.token)
mKeys.add(getString(R.string.key_url))
mValues.add(EhUrl.getGalleryDetailUrl(mDetail.gid, mDetail.token))
mKeys.add(getString(R.string.key_title))
mValues.add(mDetail.title)
mKeys.add(getString(R.string.key_title_jpn))
mValues.add(mDetail.titleJpn)
mKeys.add(getString(R.string.key_thumb))
mValues.add(mDetail.thumb)
mKeys.add(getString(R.string.key_category))
mValues.add(EhUtils.getCategory(mDetail.category))
mKeys.add(getString(R.string.key_uploader))
mValues.add(mDetail.uploader)
mKeys.add(getString(R.string.key_posted))
mValues.add(mDetail.posted)
mKeys.add(getString(R.string.key_parent))
mValues.add(mDetail.parent)
mKeys.add(getString(R.string.key_visible))
mValues.add(mDetail.visible)
mKeys.add(getString(R.string.key_language))
mValues.add(mDetail.language)
mKeys.add(getString(R.string.key_pages))
mValues.add(mDetail.pages.toString())
mKeys.add(getString(R.string.key_size))
mValues.add(mDetail.size)
mKeys.add(getString(R.string.key_favorite_count))
mValues.add(mDetail.favoriteCount.toString())
mKeys.add(getString(R.string.key_favorited))
mValues.add(java.lang.Boolean.toString(mDetail.isFavorited))
mKeys.add(getString(R.string.key_rating_count))
mValues.add(mDetail.ratingCount.toString())
mKeys.add(getString(R.string.key_rating))
mValues.add(mDetail.rating.toString())
mKeys.add(getString(R.string.key_torrents))
mValues.add(mDetail.torrentCount.toString())
mKeys.add(getString(R.string.key_torrent_url))
mValues.add(mDetail.torrentUrl)
mKeys.add(getString(R.string.favorite_name))
mValues.add(mDetail.favoriteName)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(view.findViewById(R.id.title) as TextView).setText(R.string.gallery_info)
}
override fun onDestroyView() {
super.onDestroyView()
mRecyclerView?.stopScroll()
}
fun onItemClick(position: Int): Boolean {
val context = context
return if (null != context && 0 != position) {
if (position == INDEX_PARENT) {
UrlOpener.openUrl(context, mValues[position], true)
} else {
activity?.addTextToClipboard(
mValues[position],
false,
)
if (position == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(mValues[position].hashCode())
}
}
true
} else {
false
}
}
private class InfoHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val key: TextView
val value: TextView
init {
key = itemView.findViewById(R.id.key)
value = itemView.findViewById(R.id.value)
}
}
private inner class InfoAdapter : RecyclerView.Adapter<InfoHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun getItemViewType(position: Int): Int {
return if (position == 0) {
TYPE_HEADER
} else {
TYPE_DATA
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder {
return InfoHolder(
mInflater.inflate(
if (viewType == TYPE_HEADER) R.layout.item_gallery_info_header else R.layout.item_gallery_info_data,
parent,
false
)
)
}
override fun onBindViewHolder(holder: InfoHolder, position: Int) {
holder.key.text = mKeys[position]
holder.value.text = mValues[position]
holder.itemView.isEnabled = position != 0
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return mKeys.size.coerceAtMost(mValues.size)
}
}
companion object {
const val TAG = "GalleryInfoBottomSheet"
private const val INDEX_URL = 3
private const val INDEX_PARENT = 10
private const val TYPE_HEADER = 0
private const val TYPE_DATA = 1
}
}
@@ -61,7 +61,6 @@ import com.hippo.ehviewer.ui.scene.DownloadsScene;
import com.hippo.ehviewer.ui.scene.FavoritesScene;
import com.hippo.ehviewer.ui.scene.GalleryCommentsScene;
import com.hippo.ehviewer.ui.scene.GalleryDetailScene;
import com.hippo.ehviewer.ui.scene.GalleryInfoScene;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.ehviewer.ui.scene.GalleryPreviewsScene;
import com.hippo.ehviewer.ui.scene.HistoryScene;
@@ -101,7 +100,6 @@ public final class MainActivity extends StageActivity
registerLaunchMode(SelectSiteScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(GalleryListScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TOP);
registerLaunchMode(GalleryDetailScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryInfoScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryCommentsScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(GalleryPreviewsScene.class, SceneFragment.LAUNCH_MODE_STANDARD);
registerLaunchMode(DownloadsScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
@@ -232,7 +230,6 @@ public final class MainActivity extends StageActivity
builder.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
builder.setImagePath(temp.getPath());
builder.setUseSimilarityScan(true);
builder.setShowExpunged(true);
startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)));
return true;
}
@@ -259,7 +256,7 @@ public final class MainActivity extends StageActivity
String url = intent.getData().toString();
new EditTextDialogBuilder(this, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy, (dialog, which) -> ClipboardUtilKt.addTextToClipboard(ClipboardUtilKt.getClipboardManager(this), url, false, () -> null))
.setPositiveButton(android.R.string.copy, (dialog, which) -> ClipboardUtilKt.addTextToClipboard(this, url, false))
.show();
}
}
@@ -480,8 +477,7 @@ public final class MainActivity extends StageActivity
@SuppressLint("RtlHardcoded")
public void createDrawerView(SceneFragment scene) {
if (scene instanceof BaseScene && mRightDrawer != null && mDrawerLayout != null) {
BaseScene baseScene = (BaseScene) scene;
if (scene instanceof BaseScene baseScene && mRightDrawer != null && mDrawerLayout != null) {
mRightDrawer.removeAllViews();
View drawerView = baseScene.createDrawerView(
baseScene.getLayoutInflater(), mRightDrawer, null);
@@ -498,8 +494,7 @@ public final class MainActivity extends StageActivity
public void onSceneViewDestroyed(SceneFragment scene) {
super.onSceneViewDestroyed(scene);
if (scene instanceof BaseScene) {
BaseScene baseScene = (BaseScene) scene;
if (scene instanceof BaseScene baseScene) {
baseScene.destroyDrawerView();
}
}
@@ -18,21 +18,29 @@ package com.hippo.ehviewer.ui
import android.content.res.Configuration
import android.os.Bundle
import android.view.MenuItem
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.fragment.SettingsFragment
import com.hippo.ehviewer.ui.scene.BaseScene
class SettingsActivity : EhActivity() {
private var mFab: FloatingActionButton? = null
private var mContentText: TextView? = null
private var mAppbarLayout: AppBarLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preference)
setSupportActionBar(findViewById(R.id.toolbar))
val bar = supportActionBar
bar?.setDisplayHomeAsUpEnabled(true)
mContentText = findViewById(R.id.tip)
mFab = findViewById(R.id.fab)
mAppbarLayout = findViewById(R.id.appbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) <= 0
if (savedInstanceState == null) {
@@ -62,4 +70,16 @@ class SettingsActivity : EhActivity() {
}
return super.onOptionsItemSelected(item)
}
fun getMFab(): FloatingActionButton {
return mFab!!
}
fun getMContentText(): TextView {
return mContentText!!
}
fun resetAppbarLiftStatus() {
mAppbarLayout?.setExpanded(true)
}
}
@@ -17,7 +17,6 @@
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;
@@ -59,7 +58,6 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -71,7 +69,6 @@ public class AdvancedFragment extends BasePreferenceFragment {
private static final String KEY_EXPORT_DATA = "export_data";
private static final String KEY_OPEN_BY_DEFAULT = "open_by_default";
private static final String KEY_BACKUP_FAVORITE = "backup_favorite";
ActivityResultLauncher<String> exportLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument(),
uri -> {
@@ -227,6 +224,8 @@ public class AdvancedFragment extends BasePreferenceFragment {
}
}
});
private int favTotal;
private int favIndex;
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
@@ -238,8 +237,8 @@ public class AdvancedFragment extends BasePreferenceFragment {
appLanguage.setVisible(false);
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);
Preference openByDefault = findPreference(KEY_OPEN_BY_DEFAULT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
openByDefault.setVisible(false);
@@ -282,6 +281,14 @@ public class AdvancedFragment extends BasePreferenceFragment {
showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT);
}
return true;
} else if (KEY_BACKUP_FAVORITE.equals(key)) {
try {
backupFavorite();
} catch (Exception e) {
ExceptionUtils.throwIfFatal(e);
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
return true;
} else if (KEY_OPEN_BY_DEFAULT.equals(key)) {
try {
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
@@ -293,26 +300,15 @@ public class AdvancedFragment extends BasePreferenceFragment {
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);
EhClient mClient = EhApplication.getEhClient(requireContext());
FavListUrlBuilder favListUrlBuilder = new FavListUrlBuilder();
// favListUrlBuilder.setNext(page.get());
favTotal = 0;
favIndex = 1;
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
@@ -320,31 +316,53 @@ public class AdvancedFragment extends BasePreferenceFragment {
@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);
FavoritesParser.Result favResult = (FavoritesParser.Result) result;
if (favResult.galleryInfoList.isEmpty()) {
showTip(R.string.settings_advanced_backup_favorite_nothing, BaseScene.LENGTH_SHORT);
} else {
showTip(R.string.backup_favorite_success, BaseScene.LENGTH_SHORT);
if (favTotal == 0 && favResult.countArray != null) {
int totalFav = 0;
for (int i = 0; i < 10; i++) {
totalFav = totalFav + favResult.countArray[i];
}
favTotal = (int) Math.ceil((double) totalFav / favResult.galleryInfoList.size());
}
String status = "(" + favIndex + "/" + favTotal + ")";
showTip(GetText.getString(R.string.settings_advanced_backup_favorite_start, status), BaseScene.LENGTH_SHORT);
Log.d("LocalFavorites", "now backup page " + status);
EhDB.putLocalFavorites(favResult.galleryInfoList);
if (favResult.nextPage != null) {
try {
Thread.sleep(Settings.getDownloadDelay());
} catch (InterruptedException e) {
e.printStackTrace();
}
favIndex++;
favListUrlBuilder.setNext(favResult.nextPage);
request.setArgs(favListUrlBuilder.build(), Settings.getShowJpnTitle());
mClient.execute(request);
} else {
showTip(R.string.settings_advanced_backup_favorite_success, BaseScene.LENGTH_SHORT);
}
}
} catch (Exception e) {
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
}
@Override
public void onFailure(Exception e) {
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
@Override
public void onCancel() {
showTip(R.string.backup_favorite_failed, BaseScene.LENGTH_SHORT);
showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT);
}
});
request.setArgs(favListUrlBuilder.build(), Settings.getShowJpnTitle());
mClient.execute(request);
}
@@ -1,50 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import com.hippo.ehviewer.ui.SettingsActivity;
public class BaseFragment extends Fragment {
@Override
public void onStart() {
super.onStart();
setTitle(getFragmentTitle());
}
@StringRes
public int getFragmentTitle() {
return -1;
}
private void setTitle(@StringRes int string) {
requireActivity().setTitle(string);
}
public void showTip(@StringRes int id, int length) {
((SettingsActivity) requireActivity()).showTip(getString(id), length);
}
public void showTip(CharSequence message, int length) {
((SettingsActivity) requireActivity()).showTip(message, length);
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.ehviewer.ui.SettingsActivity
abstract class BaseFragment : Fragment() {
override fun onStart() {
super.onStart()
setTitle(getFragmentTitle())
(requireActivity() as SettingsActivity).resetAppbarLiftStatus()
}
override fun onDestroyView() {
hideFabAndClearTip()
super.onDestroyView()
}
abstract fun getFragmentTitle(): Int
private fun setTitle(@StringRes string: Int) {
requireActivity().setTitle(string)
}
fun showTip(@StringRes id: Int, length: Int) {
(requireActivity() as SettingsActivity).showTip(getString(id), length)
}
fun showTip(message: CharSequence?, length: Int) {
(requireActivity() as SettingsActivity).showTip(message, length)
}
fun getTipView(@StringRes res: Int? = null): TextView {
return (requireActivity() as SettingsActivity).getMContentText().apply {
visibility = View.VISIBLE
res?.let { text = getString(it) }
}
}
fun getFabViewAndShow(): FloatingActionButton {
return (requireActivity() as SettingsActivity).getMFab().apply { visibility = View.VISIBLE }
}
private fun hideFabAndClearTip() {
getFabViewAndShow().apply {
visibility = View.GONE
setOnClickListener(null)
}
getTipView().apply {
visibility = View.GONE
}
}
}
@@ -20,7 +20,12 @@ package com.hippo.ehviewer.ui.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
@@ -31,10 +36,8 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.dao.Filter
@@ -66,26 +69,23 @@ class FilterFragment : BaseFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.activity_filter, container, false)
val recyclerView: RecyclerView =
ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView
val tip = ViewUtils.`$$`(view, R.id.tip) as TextView
): View {
val recyclerView =
inflater.inflate(R.layout.rv_layout, container, false) as RecyclerView
val tip = getTipView(R.string.filter)
mViewTransition = ViewTransition(recyclerView, tip)
val fab = view.findViewById<FloatingActionButton>(R.id.fab)
val fab = getFabViewAndShow()
val drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable?.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mAdapter.setHasStableIds(true)
recyclerView.adapter = mAdapter
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.setHasFixedSize(true)
val defaultItemAnimator = recyclerView.itemAnimator as DefaultItemAnimator?
defaultItemAnimator?.supportsChangeAnimations = false
fab.setOnClickListener { showAddFilterDialog() }
return view
return recyclerView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -106,9 +106,9 @@ class FilterFragment : BaseFragment() {
}
override fun onDestroyView() {
super.onDestroyView()
mViewTransition = null
requireActivity().removeMenuProvider(mMenuProvider)
super.onDestroyView()
}
private fun showTipDialog() {
@@ -1,283 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Hosts;
import com.hippo.ehviewer.R;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.ViewUtils;
import java.util.List;
import java.util.Locale;
public class HostsFragment extends BaseFragment
implements View.OnClickListener {
private static final String DIALOG_TAG_ADD_HOST = AddHostDialogFragment.class.getName();
private static final String DIALOG_TAG_EDIT_HOST = EditHostDialogFragment.class.getName();
private static final String KEY_HOST = "com.hippo.ehviewer.ui.fragment.HostsFragment.HOST";
private static final String KEY_IP = "com.hippo.ehviewer.ui.fragment.HostsFragment.IP";
private Hosts hosts;
private List<Pair<String, String>> data;
private ViewTransition mViewTransition;
private HostsAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hosts = EhApplication.getHosts(requireContext());
data = hosts.getAll();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.activity_hosts, container, false);
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(recyclerView, tip);
FloatingActionButton fab = view.findViewById(R.id.fab);
adapter = new HostsAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false));
recyclerView.setHasFixedSize(true);
fab.setOnClickListener(this);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
updateView(false);
}
public boolean onItemClick(int position) {
Pair<String, String> pair = data.get(position);
Bundle args = new Bundle();
args.putString(KEY_HOST, pair.first);
args.putString(KEY_IP, pair.second);
DialogFragment fragment = new EditHostDialogFragment();
fragment.setArguments(args);
fragment.show(getChildFragmentManager(), DIALOG_TAG_EDIT_HOST);
return true;
}
@Override
public void onClick(View v) {
new AddHostDialogFragment().show(getChildFragmentManager(), DIALOG_TAG_ADD_HOST);
}
private void updateView(boolean animation) {
if (null == mViewTransition) {
return;
}
data = hosts.getAll();
if (data.isEmpty()) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
adapter.notifyDataSetChanged();
}
@Override
public int getFragmentTitle() {
return R.string.hosts;
}
public abstract static class HostDialogFragment extends DialogFragment {
private HostsFragment hostsFragment;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
hostsFragment = (HostsFragment) getParentFragment();
View view = getLayoutInflater().inflate(R.layout.dialog_hosts, null, false);
TextView host = view.findViewById(R.id.host);
TextView ip = view.findViewById(R.id.ip);
Bundle arguments = getArguments();
if (savedInstanceState == null && arguments != null) {
host.setText(arguments.getString(KEY_HOST));
ip.setText(arguments.getString(KEY_IP));
}
BaseDialogBuilder builder = new BaseDialogBuilder(requireContext());
builder.setView(view);
onCreateDialogBuilder(builder);
AlertDialog dialog = builder.create();
dialog.setOnShowListener(d -> onCreateDialog((AlertDialog) d));
return dialog;
}
protected abstract void onCreateDialogBuilder(AlertDialog.Builder builder);
protected abstract void onCreateDialog(AlertDialog dialog);
protected void put(AlertDialog dialog) {
TextView host = dialog.findViewById(R.id.host);
TextView ip = dialog.findViewById(R.id.ip);
if (host == null || ip == null) {
return;
}
String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
String ipString = ip.getText().toString().trim();
if (!Hosts.isValidHost(hostString)) {
TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout);
if (hostInputLayout == null) {
return;
}
hostInputLayout.setError(getString(R.string.invalid_host));
return;
}
if (!Hosts.isValidIp(ipString)) {
TextInputLayout ipInputLayout = dialog.findViewById(R.id.ip_input_layout);
if (ipInputLayout == null) {
return;
}
ipInputLayout.setError(getString(R.string.invalid_ip));
return;
}
hostsFragment.hosts.put(hostString, ipString);
hostsFragment.updateView(true);
dialog.dismiss();
}
protected void delete(AlertDialog dialog) {
TextView host = dialog.findViewById(R.id.host);
if (host == null) {
return;
}
String hostString = host.getText().toString().trim().toLowerCase(Locale.US);
hostsFragment.hosts.delete(hostString);
hostsFragment.updateView(true);
dialog.dismiss();
}
}
public static class AddHostDialogFragment extends HostDialogFragment {
@Override
protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(R.string.add_host);
builder.setPositiveButton(R.string.add_host_add, null);
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onCreateDialog(AlertDialog dialog) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
}
}
public static class EditHostDialogFragment extends HostDialogFragment {
@Override
protected void onCreateDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(R.string.edit_host);
builder.setPositiveButton(R.string.edit_host_confirm, null);
builder.setNegativeButton(R.string.edit_host_delete, null);
}
@Override
protected void onCreateDialog(AlertDialog dialog) {
TextInputLayout hostInputLayout = dialog.findViewById(R.id.host_input_layout);
if (hostInputLayout == null) {
return;
}
hostInputLayout.setEnabled(false);
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> put(dialog));
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> delete(dialog));
}
}
private static class HostsHolder extends RecyclerView.ViewHolder {
public final TextView host;
public final TextView ip;
public HostsHolder(View itemView) {
super(itemView);
host = itemView.findViewById(R.id.host);
ip = itemView.findViewById(R.id.ip);
}
}
private class HostsAdapter extends RecyclerView.Adapter<HostsHolder> {
private final LayoutInflater inflater = getLayoutInflater();
@NonNull
@Override
public HostsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false));
}
@Override
public void onBindViewHolder(@NonNull HostsHolder holder, int position) {
Pair<String, String> pair = data.get(position);
holder.host.setText(pair.first);
holder.ip.setText(pair.second);
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
}
@@ -0,0 +1,228 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Pair
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.Hosts
import com.hippo.ehviewer.R
import com.hippo.view.ViewTransition
class HostsFragment : BaseFragment(), View.OnClickListener {
private var hosts: Hosts? = null
private var data: List<Pair<String, String>>? = null
private var mViewTransition: ViewTransition? = null
private var adapter: HostsAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EhApplication.getHosts(requireContext()).apply {
hosts = this
data = all
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val recyclerView = inflater.inflate(R.layout.rv_layout, container, false) as RecyclerView
val tip = getTipView(R.string.hosts)
mViewTransition = ViewTransition(recyclerView, tip)
val fab = getFabViewAndShow()
adapter = HostsAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager =
LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false)
recyclerView.setHasFixedSize(true)
fab.setOnClickListener(this)
return recyclerView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateView(false)
}
fun onItemClick(position: Int): Boolean {
val pair = data!![position]
val args = Bundle()
args.putString(KEY_HOST, pair.first)
args.putString(KEY_IP, pair.second)
val fragment: DialogFragment = EditHostDialogFragment()
fragment.arguments = args
fragment.show(childFragmentManager, DIALOG_TAG_EDIT_HOST)
return true
}
override fun onClick(v: View) {
AddHostDialogFragment().show(childFragmentManager, DIALOG_TAG_ADD_HOST)
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
data = hosts!!.all
if ((data as MutableList<Pair<String, String>>?)?.isEmpty() == true) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
adapter!!.notifyDataSetChanged()
}
override fun getFragmentTitle(): Int {
return R.string.hosts
}
abstract class HostDialogFragment : DialogFragment() {
private var hostsFragment: HostsFragment? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
hostsFragment = parentFragment as HostsFragment?
val view = layoutInflater.inflate(R.layout.dialog_hosts, null, false)
val host = view.findViewById<TextView>(R.id.host)
val ip = view.findViewById<TextView>(R.id.ip)
val arguments = arguments
if (savedInstanceState == null && arguments != null) {
host.text = arguments.getString(KEY_HOST)
ip.text = arguments.getString(KEY_IP)
}
val builder = BaseDialogBuilder(requireContext())
builder.setView(view)
onCreateDialogBuilder(builder)
val dialog = builder.create()
dialog.setOnShowListener { d: DialogInterface -> onCreateDialog(d as AlertDialog) }
return dialog
}
protected abstract fun onCreateDialogBuilder(builder: AlertDialog.Builder)
protected abstract fun onCreateDialog(dialog: AlertDialog)
protected fun put(dialog: AlertDialog) {
val host = dialog.findViewById<TextView>(R.id.host)
val ip = dialog.findViewById<TextView>(R.id.ip)
if (host == null || ip == null) {
return
}
val hostString = host.text.toString().trim { it <= ' ' }.lowercase()
val ipString = ip.text.toString().trim { it <= ' ' }
if (!Hosts.isValidHost(hostString)) {
val hostInputLayout =
dialog.findViewById<TextInputLayout>(R.id.host_input_layout) ?: return
hostInputLayout.error = getString(R.string.invalid_host)
return
}
if (!Hosts.isValidIp(ipString)) {
val ipInputLayout =
dialog.findViewById<TextInputLayout>(R.id.ip_input_layout) ?: return
ipInputLayout.error = getString(R.string.invalid_ip)
return
}
hostsFragment!!.hosts!!.put(hostString, ipString)
hostsFragment!!.updateView(true)
dialog.dismiss()
}
protected fun delete(dialog: AlertDialog) {
val host = dialog.findViewById<TextView>(R.id.host) ?: return
val hostString = host.text.toString().trim { it <= ' ' }.lowercase()
hostsFragment!!.hosts!!.delete(hostString)
hostsFragment!!.updateView(true)
dialog.dismiss()
}
}
class AddHostDialogFragment : HostDialogFragment() {
override fun onCreateDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(R.string.add_host)
builder.setPositiveButton(R.string.add_host_add, null)
builder.setNegativeButton(android.R.string.cancel, null)
}
override fun onCreateDialog(dialog: AlertDialog) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener { put(dialog) }
}
}
class EditHostDialogFragment : HostDialogFragment() {
override fun onCreateDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(R.string.edit_host)
builder.setPositiveButton(R.string.edit_host_confirm, null)
builder.setNegativeButton(R.string.edit_host_delete, null)
}
override fun onCreateDialog(dialog: AlertDialog) {
val hostInputLayout =
dialog.findViewById<TextInputLayout>(R.id.host_input_layout) ?: return
hostInputLayout.isEnabled = false
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
.setOnClickListener { put(dialog) }
dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
.setOnClickListener { delete(dialog) }
}
}
private class HostsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val host: TextView
val ip: TextView
init {
host = itemView.findViewById(R.id.host)
ip = itemView.findViewById(R.id.ip)
}
}
private inner class HostsAdapter : RecyclerView.Adapter<HostsHolder>() {
private val inflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HostsHolder {
return HostsHolder(inflater.inflate(R.layout.item_hosts, parent, false))
}
override fun onBindViewHolder(holder: HostsHolder, position: Int) {
val pair = data!![position]
holder.host.text = pair.first
holder.ip.text = pair.second
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return data!!.size
}
}
companion object {
private val DIALOG_TAG_ADD_HOST = AddHostDialogFragment::class.java.name
private val DIALOG_TAG_EDIT_HOST = EditHostDialogFragment::class.java.name
private const val KEY_HOST = "com.hippo.ehviewer.ui.fragment.HostsFragment.HOST"
private const val KEY_IP = "com.hippo.ehviewer.ui.fragment.HostsFragment.IP"
}
}
@@ -42,8 +42,6 @@ import androidx.fragment.app.FragmentActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import kotlin.Unit;
public abstract class BaseScene extends SceneFragment {
public static final int LENGTH_SHORT = 0;
@@ -108,12 +106,11 @@ public abstract class BaseScene extends SceneFragment {
}
}
public Unit showTip(@StringRes int id, int length) {
public void showTip(@StringRes int id, int length) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(id, length);
}
return null;
}
public boolean needShowLeftDrawer() {
@@ -0,0 +1,21 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.hippo.ehviewer.R
abstract class CollapsingToolbarScene : ToolbarScene() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view =
inflater.inflate(R.layout.scene_collapsing_toolbar, container, false) as ViewGroup
mToolbar = view.findViewById(R.id.toolbar)
mAppBarLayout = view.findViewById(R.id.appbar)
val contentView = onCreateViewWithToolbar(inflater, view, savedInstanceState)
return view.apply { addView(contentView, 0) }
}
}
@@ -311,8 +311,14 @@ public class DownloadsScene extends ToolbarScene
View content = ViewUtils.$$(view, R.id.content);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(content, R.id.recycler_view);
setLiftOnScrollTargetView(mRecyclerView);
mFastScroller = (FastScroller) ViewUtils.$$(content, R.id.fast_scroller);
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
// Workaround
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
container.addView(mFabLayout);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(content, mTip);
@@ -388,6 +394,7 @@ public class DownloadsScene extends ToolbarScene
}
if (null != mFabLayout) {
removeAboveSnackView(mFabLayout);
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
mFabLayout = null;
}
@@ -23,7 +23,6 @@ import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.Gravity;
@@ -34,7 +33,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
@@ -43,10 +41,14 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.datepicker.CalendarConstraints;
import com.google.android.material.datepicker.CompositeDateValidator;
import com.google.android.material.datepicker.DateValidatorPointBackward;
import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.annotation.Implemented;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
@@ -79,6 +81,9 @@ import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.ViewUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -113,19 +118,6 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@ViewLifeCircle
private FabLayout mFabLayout;
private final Runnable showNormalFabsRunnable = new Runnable() {
@Override
public void run() {
if (mFabLayout != null) {
mFabLayout.setSecondaryFabVisibilityAt(0, true);
mFabLayout.setSecondaryFabVisibilityAt(1, true);
mFabLayout.setSecondaryFabVisibilityAt(2, true);
mFabLayout.setSecondaryFabVisibilityAt(3, false);
mFabLayout.setSecondaryFabVisibilityAt(4, false);
mFabLayout.setSecondaryFabVisibilityAt(5, false);
}
}
};
@Nullable
@ViewLifeCircle
private FavoritesAdapter mAdapter;
@@ -156,6 +148,21 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@WholeLifeCircle
private FavListUrlBuilder mUrlBuilder;
private final Runnable showNormalFabsRunnable = new Runnable() {
@Override
public void run() {
if (mFabLayout != null) {
mFabLayout.setSecondaryFabVisibilityAt(0, true);
mFabLayout.setSecondaryFabVisibilityAt(1, true);
mFabLayout.setSecondaryFabVisibilityAt(2, true);
mFabLayout.setSecondaryFabVisibilityAt(3, false);
mFabLayout.setSecondaryFabVisibilityAt(4, false);
mFabLayout.setSecondaryFabVisibilityAt(5, false);
mFabLayout.setSecondaryFabVisibilityAt(6, false);
}
updateJumpFab();
}
};
private int mFavLocalCount = 0;
private int mFavCountSum = 0;
private boolean mHasFirstRefresh;
@@ -282,6 +289,7 @@ public class FavoritesScene extends BaseScene implements
mSearchBar.setHelper(this);
mSearchBar.setAllowEmptySearch(false);
updateSearchBar();
updateJumpFab();
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView);
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
@@ -350,6 +358,13 @@ public class FavoritesScene extends BaseScene implements
Settings.putRecentFavCat(mUrlBuilder.getFavCat());
}
// Hide jump fab on local fav cat
private void updateJumpFab() {
if (mFabLayout != null && mUrlBuilder != null) {
mFabLayout.setSecondaryFabVisibilityAt(1, mUrlBuilder.getFavCat() != FavListUrlBuilder.FAV_CAT_LOCAL);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -485,6 +500,7 @@ public class FavoritesScene extends BaseScene implements
mUrlBuilder.setKeyword(null);
mUrlBuilder.setFavCat(newFavCat);
updateSearchBar();
updateJumpFab();
mHelper.refresh();
closeDrawer(Gravity.RIGHT);
@@ -645,35 +661,27 @@ public class FavoritesScene extends BaseScene implements
return;
}
final int page = mHelper.getPageForTop();
final int pages = mHelper.getPages();
String hint = getString(R.string.go_to_hint, page + 1, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
LocalDateTime local = LocalDateTime.of(2007, 3, 21, 0, 0);
long fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
long toDate = MaterialDatePicker.todayInUtcMilliseconds();
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goTo(goTo);
dialog.dismiss();
ArrayList listValidators = new ArrayList<>();
listValidators.add(DateValidatorPointForward.from(fromDate));
listValidators.add(DateValidatorPointBackward.before(toDate));
var constraintsBuilder = new CalendarConstraints.Builder()
.setStart(fromDate)
.setEnd(toDate)
.setValidator(CompositeDateValidator.allOf(listValidators));
var datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(constraintsBuilder.build())
.setTitleText(R.string.go_to)
.setSelection(toDate)
.build();
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
datePicker.addOnPositiveButtonClickListener(v -> {
mHelper.goTo(v);
});
}
@@ -715,7 +723,11 @@ public class FavoritesScene extends BaseScene implements
}
switch (position) {
case 3: { // Download
case 3: { // Check all
mRecyclerView.checkAll();
break;
}
case 4: { // Download
Activity activity = getMainActivity();
if (activity != null) {
CommonOperations.startDownload(getMainActivity(), mModifyGiList, false);
@@ -726,7 +738,7 @@ public class FavoritesScene extends BaseScene implements
}
break;
}
case 4: { // Delete
case 5: { // Delete
DeleteDialogHelper helper = new DeleteDialogHelper();
new BaseDialogBuilder(context)
.setTitle(R.string.delete_favorites_dialog_title)
@@ -736,7 +748,7 @@ public class FavoritesScene extends BaseScene implements
.show();
break;
}
case 5: { // Move
case 6: { // Move
MoveDialogHelper helper = new MoveDialogHelper();
// First is local favorite, the other 10 is cloud favorite
String[] array = new String[11];
@@ -768,6 +780,7 @@ public class FavoritesScene extends BaseScene implements
mFabLayout.setSecondaryFabVisibilityAt(3, true);
mFabLayout.setSecondaryFabVisibilityAt(4, true);
mFabLayout.setSecondaryFabVisibilityAt(5, true);
mFabLayout.setSecondaryFabVisibilityAt(6, true);
}
}
@@ -851,13 +864,28 @@ public class FavoritesScene extends BaseScene implements
}
updateSearchBar();
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;
int pages = 0;
if (result.nextPage == null)
pages = mHelper.pgCounter + 1;
else
pages = Integer.MAX_VALUE;
String prev = result.prevPage, next = result.nextPage;
switch (mHelper.operation) {
case -1:
if (prev != null) mHelper.upperPage = prev;
break;
case 1:
if (next != null) mHelper.lowerPage = next;
break;
default:
mHelper.upperPage = prev;
mHelper.lowerPage = next;
break;
}
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
if (mDrawerAdapter != null) {
@@ -1177,8 +1205,10 @@ public class FavoritesScene extends BaseScene implements
}
private class FavoritesHelper extends GalleryInfoContentHelper {
public String upperPage = null;
public String lowerPage = null;
public int operation = 0;
public int pgCounter = 0;
public String nextPg = null;
@Override
protected void getPageData(final int taskId, int type, int page) {
@@ -1226,7 +1256,7 @@ public class FavoritesScene extends BaseScene implements
url = mUrlBuilder.build();
}
mUrlBuilder.setNext(nextPg);
mUrlBuilder.setNext(lowerPage);
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_MODIFY_FAVORITES);
request.setCallback(new GetFavoritesListener(getContext(),
@@ -1239,7 +1269,25 @@ public class FavoritesScene extends BaseScene implements
final String keyword = mUrlBuilder.getKeyword();
SimpleHandler.getInstance().post(() -> onGetFavoritesLocal(keyword, taskId));
} else {
mUrlBuilder.setNext(nextPg);
String prev = null, next = null;
if (jumpTo != null) {
next = Integer.toString(minGid);
operation = 0;
} else if (page != 0) {
if (page >= mHelper.getPageForTop()) {
next = lowerPage;
operation = 1;
} else {
prev = upperPage;
operation = -1;
}
}
mUrlBuilder.setPrev(prev);
mUrlBuilder.setNext(next);
mUrlBuilder.setJumpTo(jumpTo);
jumpTo = null;
String url = mUrlBuilder.build();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_FAVORITES);
@@ -1306,20 +1354,29 @@ public class FavoritesScene extends BaseScene implements
@Override
protected void onClearData() {
super.onClearData();
nextPg = null;
}
@Override
protected void beforeRefresh() {
super.beforeRefresh();
upperPage = null;
lowerPage = null;
operation = 0;
}
@Override
protected Parcelable saveInstanceState(Parcelable superState) {
Bundle bundle = (Bundle) super.saveInstanceState(superState);
bundle.putString(KEY_NEXT_PAGE, nextPg);
bundle.putString(KEY_UPPER_PAGE, upperPage);
bundle.putString(KEY_LOWER_PAGE, lowerPage);
return bundle;
}
@Override
protected Parcelable restoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
nextPg = bundle.getString(KEY_NEXT_PAGE);
upperPage = bundle.getString(KEY_UPPER_PAGE);
lowerPage = bundle.getString(KEY_LOWER_PAGE);
return super.restoreInstanceState(state);
}
}
@@ -170,8 +170,9 @@ public final class GalleryCommentsScene extends ToolbarScene
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_comments, container, false);
ViewGroup view = (ViewGroup) inflater.inflate(R.layout.scene_gallery_comments, container, false);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
setLiftOnScrollTargetView(mRecyclerView);
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
mEditPanel = ViewUtils.$$(view, R.id.edit_panel);
mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send);
@@ -179,6 +180,15 @@ public final class GalleryCommentsScene extends ToolbarScene
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
mFab = (FloatingActionButton) ViewUtils.$$(view, R.id.fab);
mRefreshLayout = (SwipeRefreshLayout) ViewUtils.$$(view, R.id.refresh_layout);
// Workaround for fab and edittext render out of screen
view.removeView(mFabLayout);
view.removeView((View) mEditPanel.getParent());
assert container != null;
container.addView(mFabLayout);
container.addView((View) mEditPanel.getParent());
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mEditPanel,
@@ -354,7 +364,7 @@ public final class GalleryCommentsScene extends ToolbarScene
}
int id = menuId.get(which);
if (id == R.id.copy) {
ClipboardUtilKt.addTextToClipboard(ClipboardUtilKt.getClipboardManager(getContext()), comment.comment, false, () -> showTip(R.string.copied_to_clipboard, LENGTH_SHORT));
ClipboardUtilKt.addTextToClipboard(requireActivity(), comment.comment, false);
} else if (id == R.id.vote_up) {
voteComment(comment.id, 1);
} else if (id == R.id.vote_down) {
@@ -377,8 +387,7 @@ public final class GalleryCommentsScene extends ToolbarScene
}
RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
if (holder instanceof ActualCommentHolder) {
ActualCommentHolder commentHolder = (ActualCommentHolder) holder;
if (holder instanceof ActualCommentHolder commentHolder) {
ClickableSpan span = commentHolder.comment.getCurrentSpan();
commentHolder.comment.clearCurrentSpan();
@@ -31,6 +31,7 @@ import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
@@ -40,15 +41,12 @@ import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RatingBar;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
@@ -60,10 +58,12 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.widget.NestedScrollView;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.ehviewer.AppConfig;
@@ -93,17 +93,21 @@ import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.gallery.EhGalleryProvider;
import com.hippo.ehviewer.gallery.GalleryProvider2;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.annotation.WholeLifeCircle;
import com.hippo.ehviewer.widget.GalleryRatingBar;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.text.URLImageGetter;
import com.hippo.unifile.UniFile;
import com.hippo.util.AppHelper;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.util.ReadableTime;
import com.hippo.view.ViewTransition;
import com.hippo.widget.AutoWrapLayout;
@@ -114,7 +118,6 @@ import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.IntList;
@@ -130,10 +133,9 @@ import java.util.List;
import okhttp3.HttpUrl;
import rikka.core.res.ResourcesKt;
public class GalleryDetailScene extends ToolbarScene implements View.OnClickListener,
public class GalleryDetailScene extends CollapsingToolbarScene implements View.OnClickListener,
com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener,
View.OnLongClickListener {
public final static String KEY_ACTION = "action";
public static final String ACTION_GALLERY_INFO = "action_gallery_info";
public static final String ACTION_GID_TOKEN = "action_gid_token";
@@ -159,14 +161,10 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
private ViewTransition mViewTransition;
// Header
@Nullable
private FrameLayout mHeader;
@Nullable
private View mColorBg;
private LinearLayout mHeader;
@Nullable
private LoadImageView mThumb;
@Nullable
private TextView mTitle;
@Nullable
private TextView mUploader;
@Nullable
private TextView mCategory;
@@ -284,6 +282,21 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return null;
}
private static void deleteFileAsync(UniFile... files) {
//noinspection deprecation
new AsyncTask<UniFile, Void, Void>() {
@Override
protected Void doInBackground(UniFile... params) {
for (UniFile file : params) {
if (file != null) {
file.delete();
}
}
return null;
}
}.executeOnExecutor(IoThreadPoolExecutor.getInstance(), files);
}
@StringRes
private int getRatingText(float rating) {
int resId;
@@ -555,7 +568,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
View view = inflater.inflate(R.layout.scene_gallery_detail, container, false);
ViewGroup main = (ViewGroup) ViewUtils.$$(view, R.id.main);
ScrollView mainView = (ScrollView) ViewUtils.$$(main, R.id.scroll_view);
NestedScrollView mainView = (NestedScrollView) ViewUtils.$$(main, R.id.scroll_view);
setLiftOnScrollTargetView(mainView);
View progressView = ViewUtils.$$(main, R.id.progress_view);
mTip = (TextView) ViewUtils.$$(main, R.id.tip);
mViewTransition = new ViewTransition(mainView, progressView, mTip);
@@ -568,10 +582,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mTip.setCompoundDrawables(null, drawable, null, null);
mTip.setOnClickListener(this);
mHeader = (FrameLayout) ViewUtils.$$(mainView, R.id.header);
mColorBg = ViewUtils.$$(mHeader, R.id.color_bg);
mHeader = (LinearLayout) ViewUtils.$$(mainView, R.id.header);
mThumb = (LoadImageView) ViewUtils.$$(mHeader, R.id.thumb);
mTitle = (TextView) ViewUtils.$$(mHeader, R.id.title);
mUploader = (TextView) ViewUtils.$$(mHeader, R.id.uploader);
mCategory = (TextView) ViewUtils.$$(mHeader, R.id.category);
mActionGroup = (ViewGroup) ViewUtils.$$(mHeader, R.id.action_card);
@@ -625,8 +637,12 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mNoTags = (TextView) ViewUtils.$$(mTags, R.id.no_tags);
mComments = (LinearLayout) ViewUtils.$$(belowHeader, R.id.comments);
mCommentsText = (TextView) ViewUtils.$$(mComments, R.id.comments_text);
mComments.setOnClickListener(this);
if (Settings.getShowComments()) {
mCommentsText = (TextView) ViewUtils.$$(mComments, R.id.comments_text);
mComments.setOnClickListener(this);
} else {
mComments.setVisibility(View.GONE);
}
mPreviews = ViewUtils.$$(belowHeader, R.id.previews);
mGridLayout = (SimpleGridAutoSpanLayout) ViewUtils.$$(mPreviews, R.id.grid_layout);
@@ -640,11 +656,9 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
if (prepareData()) {
if (mGalleryDetail != null) {
bindViewSecond();
setTransitionName();
adjustViewVisibility(STATE_NORMAL, false);
} else if (mGalleryInfo != null) {
bindViewFirst();
setTransitionName();
adjustViewVisibility(STATE_REFRESH_HEADER, false);
} else {
adjustViewVisibility(STATE_REFRESH, false);
@@ -671,9 +685,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mViewTransition = null;
mHeader = null;
mColorBg = null;
mThumb = null;
mTitle = null;
mUploader = null;
mCategory = null;
mActionGroup = null;
@@ -765,33 +777,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return true;
}
private boolean createCircularReveal() {
if (mColorBg == null) {
return false;
}
int w = mColorBg.getWidth();
int h = mColorBg.getHeight();
if (ViewCompat.isAttachedToWindow(mColorBg) && w != 0 && h != 0) {
Resources resources = getContext().getResources();
int keylineMargin = resources.getDimensionPixelSize(R.dimen.keyline_margin);
int thumbWidth = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_width);
int thumbHeight = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_height);
int x = thumbWidth / 2 + keylineMargin;
int y = thumbHeight / 2 + keylineMargin;
int radiusX = Math.max(Math.abs(x), Math.abs(w - x));
int radiusY = Math.max(Math.abs(y), Math.abs(h - y));
float radius = (float) Math.hypot(radiusX, radiusY);
ViewAnimationUtils.createCircularReveal(mColorBg, x, y, 0, radius).setDuration(300).start();
return true;
} else {
return false;
}
}
private void adjustViewVisibility(int state, boolean animation) {
if (state == mState) {
return;
@@ -829,27 +814,20 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
mViewTransition.showView(2, animation);
break;
}
if ((oldState == STATE_INIT || oldState == STATE_FAILED || oldState == STATE_REFRESH) &&
(state == STATE_NORMAL || state == STATE_REFRESH_HEADER) && ResourcesKt.resolveBoolean(getTheme(), androidx.appcompat.R.attr.isLightTheme, false)) {
if (!createCircularReveal()) {
SimpleHandler.getInstance().post(this::createCircularReveal);
}
}
}
private void bindViewFirst() {
if (mGalleryDetail != null) {
return;
}
if (mThumb == null || mTitle == null || mUploader == null || mCategory == null) {
if (mThumb == null || mUploader == null || mCategory == null) {
return;
}
if (ACTION_GALLERY_INFO.equals(mAction) && mGalleryInfo != null) {
GalleryInfo gi = mGalleryInfo;
mThumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
mTitle.setText(EhUtils.getSuitableTitle(gi));
setTitle(EhUtils.getSuitableTitle(gi));
mUploader.setText(gi.uploader);
mUploader.setAlpha(gi.disowned ? .5f : 1f);
mCategory.setText(EhUtils.getCategory(gi.category));
@@ -897,7 +875,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
})
.show();
}
if (mThumb == null || mTitle == null || mUploader == null || mCategory == null ||
if (mThumb == null || mUploader == null || mCategory == null ||
mLanguage == null || mPages == null || mSize == null || mPosted == null ||
mFavoredTimes == null || mRatingText == null || mRating == null || mTorrent == null || mNewerVersion == null) {
return;
@@ -906,7 +884,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
Resources resources = getResources();
mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb);
mTitle.setText(EhUtils.getSuitableTitle(gd));
setTitle(EhUtils.getSuitableTitle(gd));
mUploader.setText(gd.uploader);
mUploader.setAlpha(gd.disowned ? .5f : 1f);
mCategory.setText(EhUtils.getCategory(gd.category));
@@ -1035,6 +1013,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
private void bindPreviews(GalleryDetail gd) {
LayoutInflater inflater = getLayoutInflater();
Resources resources = getResourcesOrNull();
int previewNum = Settings.getPreviewNum();
if (null == inflater || null == resources || null == mGridLayout || null == mPreviewText) {
return;
}
@@ -1044,7 +1023,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
if (gd.previewPages <= 0 || previewSet == null || previewSet.size() == 0) {
mPreviewText.setText(R.string.no_previews);
return;
} else if (gd.previewPages == 1) {
} else if (gd.previewPages == 1 && previewSet.size() <= previewNum) {
mPreviewText.setText(R.string.no_more_previews);
} else {
mPreviewText.setText(R.string.more_previews);
@@ -1053,7 +1032,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
int columnWidth = resources.getDimensionPixelOffset(Settings.getThumbSizeResId());
mGridLayout.setColumnSize(columnWidth);
mGridLayout.setStrategy(SimpleGridAutoSpanLayout.STRATEGY_SUITABLE_SIZE);
for (int i = 0, size = previewSet.size(); i < size; i++) {
int size = Math.min(previewNum, previewSet.size());
for (int i = 0; i < size; i++) {
View view = inflater.inflate(R.layout.item_gallery_preview, mGridLayout, false);
mGridLayout.addView(view);
@@ -1073,10 +1053,8 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
private void setTransitionName() {
long gid = getGid();
if (gid != -1 && mThumb != null &&
mTitle != null && mUploader != null && mCategory != null) {
if (gid != -1 && mThumb != null && mUploader != null && mCategory != null) {
ViewCompat.setTransitionName(mThumb, TransitionNameFactory.getThumbTransitionName(gid));
ViewCompat.setTransitionName(mTitle, TransitionNameFactory.getTitleTransitionName(gid));
ViewCompat.setTransitionName(mUploader, TransitionNameFactory.getUploaderTransitionName(gid));
ViewCompat.setTransitionName(mCategory, TransitionNameFactory.getCategoryTransitionName(gid));
}
@@ -1134,7 +1112,6 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
lub.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
lub.setImagePath(temp.getPath());
lub.setUseSimilarityScan(true);
lub.setShowExpunged(true);
GalleryListScene.startScene(this, lub);
}
} catch (FileNotFoundException e) {
@@ -1179,10 +1156,14 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
if (EhApplication.getDownloadManager(context).getDownloadState(galleryInfo.gid) == DownloadInfo.STATE_INVALID) {
CommonOperations.startDownload(activity, galleryInfo, false);
} else {
new BaseDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(getString(R.string.download_remove_dialog_message, galleryInfo.title))
.setPositiveButton(android.R.string.ok, (dialog1, which1) -> EhApplication.getDownloadManager(context).deleteDownload(galleryInfo.gid))
CheckBoxDialogBuilder builder = new CheckBoxDialogBuilder(context,
getString(R.string.download_remove_dialog_message, galleryInfo.title),
getString(R.string.download_remove_dialog_check_text),
Settings.getRemoveImageFiles());
DeleteDialogHelper helper = new DeleteDialogHelper(
EhApplication.getDownloadManager(context), galleryInfo, builder);
builder.setTitle(R.string.download_remove_dialog_title)
.setPositiveButton(android.R.string.ok, helper)
.show();
}
}
@@ -1217,9 +1198,9 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
.show();
}
} else if (mInfo == v) {
Bundle args = new Bundle();
args.putParcelable(GalleryInfoScene.KEY_GALLERY_DETAIL, mGalleryDetail);
startScene(new Announcer(GalleryInfoScene.class).setArgs(args));
assert mGalleryDetail != null;
var galleryInfoBottomSheet = new GalleryInfoBottomSheet(mGalleryDetail);
galleryInfoBottomSheet.show(requireActivity().getSupportFragmentManager(), GalleryInfoBottomSheet.TAG);
} else if (mHeart == v || mHeartOutline == v) {
if (mGalleryDetail != null && !mModifingFavorites) {
boolean remove = false;
@@ -1316,8 +1297,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
}
} else {
Object o = v.getTag(R.id.tag);
if (o instanceof String) {
String tag = (String) o;
if (o instanceof String tag) {
ListUrlBuilder lub = new ListUrlBuilder();
lub.setMode(ListUrlBuilder.MODE_TAG);
lub.setKeyword(tag);
@@ -1430,9 +1410,9 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
} else if (id == R.id.add_filter) {
showFilterTagDialog(tag);
} else if (id == R.id.copy) {
ClipboardUtilKt.addTextToClipboard(ClipboardUtilKt.getClipboardManager(getContext()), tag, false, () -> showTip(R.string.copied_to_clipboard, LENGTH_SHORT));
ClipboardUtilKt.addTextToClipboard(requireActivity(), tag, false);
} else if (id == R.id.copy_trans) {
ClipboardUtilKt.addTextToClipboard(ClipboardUtilKt.getClipboardManager(getContext()), tv.getText().toString(), false, () -> showTip(R.string.copied_to_clipboard, LENGTH_SHORT));
ClipboardUtilKt.addTextToClipboard(requireActivity(), tv.getText().toString(), false);
}
}).show();
}
@@ -2135,4 +2115,38 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
EhApplication.getEhClient(context).execute(request);
}
}
private class DeleteDialogHelper implements DialogInterface.OnClickListener {
private final com.hippo.ehviewer.download.DownloadManager mDownloadManager;
private final GalleryInfo mGalleryInfo;
private final CheckBoxDialogBuilder mBuilder;
public DeleteDialogHelper(com.hippo.ehviewer.download.DownloadManager downloadManager,
GalleryInfo galleryInfo, CheckBoxDialogBuilder builder) {
mDownloadManager = downloadManager;
mGalleryInfo = galleryInfo;
mBuilder = builder;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which != DialogInterface.BUTTON_POSITIVE) {
return;
}
// Delete
if (null != mDownloadManager) {
mDownloadManager.deleteDownload(mGalleryInfo.gid);
}
// Delete image files
boolean checked = mBuilder.isChecked();
Settings.putRemoveImageFiles(checked);
if (checked) {
EhDB.removeDownloadDirname(mGalleryInfo.gid);
UniFile file = SpiderDen.getGalleryDownloadDir(mGalleryInfo);
deleteFileAsync(file);
}
}
}
}
@@ -1,264 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.UrlOpener;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
public final class GalleryInfoScene extends ToolbarScene {
public static final String KEY_GALLERY_DETAIL = "gallery_detail";
public static final String KEY_KEYS = "keys";
public static final String KEY_VALUES = "values";
private static final int INDEX_URL = 3;
private static final int INDEX_PARENT = 10;
/*---------------
Whole life cycle
---------------*/
@Nullable
private ArrayList<String> mKeys;
@Nullable
private ArrayList<String> mValues;
@Nullable
private EasyRecyclerView mRecyclerView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void handlerArgs(Bundle args) {
if (args == null) {
return;
}
GalleryDetail gd = args.getParcelable(KEY_GALLERY_DETAIL);
if (gd == null) {
return;
}
if (mKeys == null || mValues == null) {
return;
}
mKeys.add(getString(R.string.header_key));
mValues.add(getString(R.string.header_value));
mKeys.add(getString(R.string.key_gid));
mValues.add(Long.toString(gd.gid));
mKeys.add(getString(R.string.key_token));
mValues.add(gd.token);
mKeys.add(getString(R.string.key_url));
mValues.add(EhUrl.getGalleryDetailUrl(gd.gid, gd.token));
mKeys.add(getString(R.string.key_title));
mValues.add(gd.title);
mKeys.add(getString(R.string.key_title_jpn));
mValues.add(gd.titleJpn);
mKeys.add(getString(R.string.key_thumb));
mValues.add(gd.thumb);
mKeys.add(getString(R.string.key_category));
mValues.add(EhUtils.getCategory(gd.category));
mKeys.add(getString(R.string.key_uploader));
mValues.add(gd.uploader);
mKeys.add(getString(R.string.key_posted));
mValues.add(gd.posted);
mKeys.add(getString(R.string.key_parent));
mValues.add(gd.parent);
mKeys.add(getString(R.string.key_visible));
mValues.add(gd.visible);
mKeys.add(getString(R.string.key_language));
mValues.add(gd.language);
mKeys.add(getString(R.string.key_pages));
mValues.add(Integer.toString(gd.pages));
mKeys.add(getString(R.string.key_size));
mValues.add(gd.size);
mKeys.add(getString(R.string.key_favorite_count));
mValues.add(Integer.toString(gd.favoriteCount));
mKeys.add(getString(R.string.key_favorited));
mValues.add(Boolean.toString(gd.isFavorited));
mKeys.add(getString(R.string.key_rating_count));
mValues.add(Integer.toString(gd.ratingCount));
mKeys.add(getString(R.string.key_rating));
mValues.add(Float.toString(gd.rating));
mKeys.add(getString(R.string.key_torrents));
mValues.add(Integer.toString(gd.torrentCount));
mKeys.add(getString(R.string.key_torrent_url));
mValues.add(gd.torrentUrl);
mKeys.add(getString(R.string.favorite_name));
mValues.add(gd.favoriteName);
}
protected void onInit() {
mKeys = new ArrayList<>();
mValues = new ArrayList<>();
handlerArgs(getArguments());
}
protected void onRestore(@NonNull Bundle savedInstanceState) {
mKeys = savedInstanceState.getStringArrayList(KEY_KEYS);
mValues = savedInstanceState.getStringArrayList(KEY_VALUES);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putStringArrayList(KEY_KEYS, mKeys);
outState.putStringArrayList(KEY_VALUES, mValues);
}
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_info, container, false);
Context context = getContext();
AssertUtils.assertNotNull(context);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
InfoAdapter adapter = new InfoAdapter();
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false));
mRecyclerView.setClipToPadding(false);
mRecyclerView.setHasFixedSize(true);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_info);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
}
public boolean onItemClick(int position) {
Context context = getContext();
if (null != context && 0 != position && null != mValues) {
if (position == INDEX_PARENT) {
UrlOpener.openUrl(context, mValues.get(position), true);
} else {
ClipboardUtilKt.addTextToClipboard(ClipboardUtilKt.getClipboardManager(getContext()), mValues.get(position), false, () -> showTip(R.string.copied_to_clipboard, LENGTH_SHORT));
if (position == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(mValues.get(position).hashCode());
}
}
return true;
} else {
return false;
}
}
@Override
public void onNavigationClick() {
onBackPressed();
}
private static class InfoHolder extends RecyclerView.ViewHolder {
private final TextView key;
private final TextView value;
public InfoHolder(View itemView) {
super(itemView);
key = itemView.findViewById(R.id.key);
value = itemView.findViewById(R.id.value);
}
}
private class InfoAdapter extends RecyclerView.Adapter<InfoHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_DATA = 1;
private final LayoutInflater mInflater;
public InfoAdapter() {
mInflater = getLayoutInflater();
AssertUtils.assertNotNull(mInflater);
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else {
return TYPE_DATA;
}
}
@NonNull
@Override
public InfoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new InfoHolder(mInflater.inflate(viewType == TYPE_HEADER ?
R.layout.item_gallery_info_header : R.layout.item_gallery_info_data, parent, false));
}
@Override
public void onBindViewHolder(@NonNull InfoHolder holder, int position) {
if (mKeys != null && mValues != null) {
holder.key.setText(mKeys.get(position));
holder.value.setText(mValues.get(position));
holder.itemView.setEnabled(position != 0);
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
}
@Override
public int getItemCount() {
return mKeys == null || mValues == null ? 0 : Math.min(mKeys.size(), mValues.size());
}
}
}
@@ -55,6 +55,11 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.datepicker.CalendarConstraints;
import com.google.android.material.datepicker.CompositeDateValidator;
import com.google.android.material.datepicker.DateValidatorPointBackward;
import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
@@ -108,6 +113,9 @@ import com.hippo.yorozuya.ViewUtils;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -135,6 +143,8 @@ public final class GalleryListScene extends BaseScene
private final static int STATE_SEARCH = 2;
private final static int STATE_SEARCH_SHOW_LIST = 3;
private static final long ANIMATE_TIME = 300L;
// Double click back exit
private final long mPressBackTime = 0;
/*---------------
Whole life cycle
---------------*/
@@ -215,8 +225,6 @@ public final class GalleryListScene extends BaseScene
}
}
};
// Double click back exit
private final long mPressBackTime = 0;
private boolean mHasFirstRefresh = false;
private int mNavCheckedId = 0;
private DownloadManager mDownloadManager;
@@ -492,6 +500,8 @@ public final class GalleryListScene extends BaseScene
mFabLayout.getSecondaryFabAt(0).setImageResource(isTopList ? R.drawable.ic_baseline_format_list_numbered_24 : R.drawable.v_magnify_x24);
}
mFabLayout.setSecondaryFabVisibilityAt(1, ListUrlBuilder.MODE_WHATS_HOT != builder.getMode());
// Update normal search mode
mSearchLayout.setNormalSearchMode(builder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION
? R.id.search_subscription_search
@@ -857,36 +867,61 @@ public final class GalleryListScene extends BaseScene
return;
}
final int page = mHelper.getPageForTop();
final int pages = mHelper.getPages();
String hint = getString(R.string.go_to_hint, page + 1, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
if (ListUrlBuilder.MODE_TOPLIST == mUrlBuilder.getMode()) {
final int page = mHelper.getPageForTop() + 1;
final int pages = 200;
String hint = getString(R.string.go_to_hint, page, pages);
final EditTextDialogBuilder builder = new EditTextDialogBuilder(context, null, hint);
builder.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
final AlertDialog dialog = builder.setTitle(R.string.go_to)
.setPositiveButton(android.R.string.ok, null)
.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> {
if (null == mHelper) {
dialog.dismiss();
return;
}
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goTo(goTo);
dialog.dismiss();
});
String text = builder.getText().trim();
int goTo;
try {
goTo = Integer.parseInt(text) - 1;
} catch (NumberFormatException e) {
builder.setError(getString(R.string.error_invalid_number));
return;
}
if (goTo < 0 || goTo >= pages) {
builder.setError(getString(R.string.error_out_of_range));
return;
}
builder.setError(null);
mHelper.goToPage(goTo);
dialog.dismiss();
});
} else {
LocalDateTime local = LocalDateTime.of(2007, 3, 21, 0, 0);
long fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli();
long toDate = MaterialDatePicker.todayInUtcMilliseconds();
ArrayList listValidators = new ArrayList<>();
listValidators.add(DateValidatorPointForward.from(fromDate));
listValidators.add(DateValidatorPointBackward.before(toDate));
var constraintsBuilder = new CalendarConstraints.Builder()
.setStart(fromDate)
.setEnd(toDate)
.setValidator(CompositeDateValidator.allOf(listValidators));
var datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(constraintsBuilder.build())
.setTitleText(R.string.go_to)
.setSelection(toDate)
.build();
datePicker.show(requireActivity().getSupportFragmentManager(), "date-picker");
datePicker.addOnPositiveButtonClickListener(v -> {
mHelper.goTo(v);
});
}
}
@Override
@@ -1200,7 +1235,7 @@ public final class GalleryListScene extends BaseScene
break;
}
}
((StageActivity)requireActivity()).updateBackPressCallBackStatus();
((StageActivity) requireActivity()).updateBackPressCallBackStatus();
}
@Override
@@ -1391,7 +1426,16 @@ public final class GalleryListScene extends BaseScene
? R.string.gallery_list_empty_hit_subscription
: R.string.gallery_list_empty_hit);
mHelper.setEmptyString(emptyString);
mHelper.onGetPageData(taskId, CommonOperations.getPagesForFounds(result.founds, 25), mHelper.pgCounter + 1, result.galleryInfoList);
int pages = 0;
if (mIsTopList)
pages = 200;
else if (result.nextGid == 0)
pages = mHelper.pgCounter + 1;
else
pages = result.founds;
mHelper.onGetPageData(taskId, pages, mHelper.pgCounter + 1, result.galleryInfoList);
}
}
@@ -1721,10 +1765,28 @@ public final class GalleryListScene extends BaseScene
return;
}
if (page != 0)
mUrlBuilder.setNextGid(minGid);
else
mUrlBuilder.setNextGid(0);
int prevGid = 0, nextGid = 0;
if (mIsTopList) {
if (jumpTo != null) {
pgCounter = Integer.parseInt(jumpTo);
nextGid = pgCounter;
} else {
nextGid = page;
}
} else if (jumpTo != null) {
nextGid = minGid;
} else if (page != 0) {
if (page >= mHelper.getPageForTop())
nextGid = minGid;
else
prevGid = maxGid;
}
mUrlBuilder.setPrevGid(prevGid);
mUrlBuilder.setNextGid(nextGid);
mUrlBuilder.setJumpTo(jumpTo);
jumpTo = null;
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.getMode()) {
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_IMAGE_SEARCH);
@@ -1837,4 +1899,4 @@ public final class GalleryListScene extends BaseScene
mAdapter.notifyDataSetChanged();
}
}
}
}
@@ -45,6 +45,7 @@ import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryPreview;
import com.hippo.ehviewer.client.data.PreviewSet;
@@ -139,6 +140,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
R.layout.scene_gallery_previews, container, false);
mContentLayout.hideFastScroll();
mRecyclerView = mContentLayout.getRecyclerView();
setLiftOnScrollTargetView(mRecyclerView);
Context context = getContext();
AssertUtils.assertNotNull(context);
@@ -189,11 +191,8 @@ public class GalleryPreviewsScene extends ToolbarScene {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_previews);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
}
@Override
public int getMenuResId() {
return R.menu.scene_gallery_previews;
if (((GalleryDetail) mGalleryInfo).previewPages > 2)
showMenu(R.menu.scene_gallery_previews);
}
@Override
@@ -25,7 +25,6 @@ import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -36,10 +35,8 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.easyrecyclerview.FastScroller
import com.hippo.easyrecyclerview.HandlerDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.*
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhUtils
@@ -63,18 +60,58 @@ import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
class HistoryScene : ToolbarScene() {
private var mRecyclerView: EasyRecyclerView? = null
private var mViewTransition: ViewTransition? = null
private var mAdapter: HistoryAdapter? = null
private var mDownloadManager: DownloadManager? = null
private var mDownloadInfoListener: DownloadInfoListener? = null
private var mFavouriteStatusRouter: FavouriteStatusRouter? = null
private var mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener? = null
private var mRecyclerView: RecyclerView? = null
private val mAdapter: HistoryAdapter by lazy {
HistoryAdapter(object : DiffUtil.ItemCallback<HistoryInfo>() {
override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
})
}
private val mDownloadManager: DownloadManager by lazy {
EhApplication.getDownloadManager(requireContext())
}
private val mDownloadInfoListener: DownloadInfoListener by lazy {
object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter.notifyDataSetChanged()
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}
override fun onUpdateAll() {}
override fun onReload() {
mAdapter.notifyDataSetChanged()
}
override fun onChange() {
mAdapter.notifyDataSetChanged()
}
override fun onRenameLabel(from: String, to: String) {}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter.notifyDataSetChanged()
}
override fun onUpdateLabels() {}
}
}
private val mFavouriteStatusRouter: FavouriteStatusRouter by lazy {
EhApplication.getFavouriteStatusRouter(requireContext())
}
private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener by lazy {
FavouriteStatusRouter.Listener { _: Long, _: Int ->
mAdapter.notifyDataSetChanged()
}
}
override fun onDestroy() {
super.onDestroy()
mDownloadManager!!.removeDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter!!.removeListener(mFavouriteStatusRouterListener)
mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener)
}
override fun getNavCheckedItem(): Int {
@@ -84,48 +121,21 @@ class HistoryScene : ToolbarScene() {
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = requireContext()
mDownloadManager = EhApplication.getDownloadManager(context)
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context)
mDownloadInfoListener = object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter?.notifyDataSetChanged()
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}
override fun onUpdateAll() {}
override fun onReload() {
mAdapter?.notifyDataSetChanged()
}
override fun onChange() {
mAdapter?.notifyDataSetChanged()
}
override fun onRenameLabel(from: String, to: String) {}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter?.notifyDataSetChanged()
}
override fun onUpdateLabels() {}
}
mDownloadManager!!.addDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouterListener = FavouriteStatusRouter.Listener { _: Long, _: Int ->
mAdapter?.notifyDataSetChanged()
}
mFavouriteStatusRouter!!.addListener(mFavouriteStatusRouterListener)
mDownloadManager.addDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
): View {
val view = inflater.inflate(R.layout.scene_history, container, false)
val content = ViewUtils.`$$`(view, R.id.content)
val recyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as EasyRecyclerView
val recyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as RecyclerView
setLiftOnScrollTargetView(recyclerView)
val mFastScroller = ViewUtils.`$$`(content, R.id.fast_scroller) as FastScroller
val mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
mViewTransition = ViewTransition(content, mTip)
ViewTransition(content, mTip)
val resources = requireContext().resources
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_history)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
@@ -135,15 +145,6 @@ class HistoryScene : ToolbarScene() {
) {
EhDB.getHistoryLazyList()
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
mAdapter = HistoryAdapter(object : DiffUtil.ItemCallback<HistoryInfo>() {
override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
})
recyclerView.adapter = mAdapter
val layoutManager = AutoStaggeredGridLayoutManager(
0, StaggeredGridLayoutManager.VERTICAL
@@ -153,11 +154,6 @@ class HistoryScene : ToolbarScene() {
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
val decoration = MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
recyclerView.addItemDecoration(decoration)
val itemTouchHelper = ItemTouchHelper(HistoryItemTouchHelperCallback())
itemTouchHelper.attachToRecyclerView(recyclerView)
mFastScroller.attachToRecyclerView(recyclerView)
@@ -167,7 +163,7 @@ class HistoryScene : ToolbarScene() {
mFastScroller.setHandlerDrawable(handlerDrawable)
viewLifecycleOwner.lifecycleScope.launch {
historyData.collectLatest { value ->
mAdapter!!.submitData(
mAdapter.submitData(
value
)
}
@@ -185,8 +181,6 @@ class HistoryScene : ToolbarScene() {
super.onDestroyView()
mRecyclerView?.stopScroll()
mRecyclerView = null
mViewTransition = null
mAdapter = null
}
@SuppressLint("RtlHardcoded")
@@ -203,20 +197,18 @@ class HistoryScene : ToolbarScene() {
BaseDialogBuilder(requireContext())
.setMessage(R.string.clear_all_history)
.setPositiveButton(R.string.clear_all) { _: DialogInterface?, which: Int ->
if (DialogInterface.BUTTON_POSITIVE != which || null == mAdapter) {
if (DialogInterface.BUTTON_POSITIVE != which) {
return@setPositiveButton
}
EhDB.clearHistoryInfo()
mAdapter!!.refresh()
mAdapter!!.notifyDataSetChanged()
mAdapter.refresh()
mAdapter.notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
// Skip when in choice mode
context ?: return false
val id = item.itemId
if (id == R.id.action_clear_all) {
showClearAllDialog()
@@ -225,20 +217,19 @@ class HistoryScene : ToolbarScene() {
return false
}
fun onItemClick(position: Int): Boolean {
fun onItemClick(gi: GalleryInfo): Boolean {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mAdapter?.peek(position))
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)
val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)
startScene(announcer)
return true
}
fun onItemLongClick(position: Int): Boolean {
fun onItemLongClick(gi: GalleryInfo): Boolean {
val context = requireContext()
val activity = mainActivity ?: return false
val gi: GalleryInfo = mAdapter?.peek(position) ?: return true
val downloaded = mDownloadManager!!.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID
val downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID
val favourited = gi.favoriteSlot != -2
val items = if (downloaded) arrayOf<CharSequence>(
context.getString(R.string.read),
@@ -276,6 +267,7 @@ class HistoryScene : ToolbarScene() {
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi)
startActivity(intent)
}
1 -> if (downloaded) {
BaseDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
@@ -286,7 +278,7 @@ class HistoryScene : ToolbarScene() {
)
)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
mDownloadManager!!.deleteDownload(
mDownloadManager.deleteDownload(
gi.gid
)
}
@@ -294,6 +286,7 @@ class HistoryScene : ToolbarScene() {
} else {
CommonOperations.startDownload(activity, gi, false)
}
2 -> if (favourited) {
CommonOperations.removeFromFavorites(
activity,
@@ -307,6 +300,7 @@ class HistoryScene : ToolbarScene() {
AddToFavoriteListener(context, activity.stageId, tag)
)
}
3 -> {
val labelRawList = EhApplication.getDownloadManager(context).labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
@@ -362,29 +356,16 @@ class HistoryScene : ToolbarScene() {
}
private class HistoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val card: View
val thumb: LoadImageView
val title: TextView
val uploader: TextView
val rating: SimpleRatingView
val category: TextView
val posted: TextView
val simpleLanguage: TextView
val pages: TextView
val downloaded: ImageView
init {
card = itemView.findViewById(R.id.card)
thumb = itemView.findViewById(R.id.thumb)
title = itemView.findViewById(R.id.title)
uploader = itemView.findViewById(R.id.uploader)
rating = itemView.findViewById(R.id.rating)
category = itemView.findViewById(R.id.category)
posted = itemView.findViewById(R.id.posted)
simpleLanguage = itemView.findViewById(R.id.simple_language)
pages = itemView.findViewById(R.id.pages)
downloaded = itemView.findViewById(R.id.downloaded)
}
val card: View = itemView.findViewById(R.id.card)
val thumb: LoadImageView = itemView.findViewById(R.id.thumb)
val title: TextView = itemView.findViewById(R.id.title)
val uploader: TextView = itemView.findViewById(R.id.uploader)
val rating: SimpleRatingView = itemView.findViewById(R.id.rating)
val category: TextView = itemView.findViewById(R.id.category)
val posted: TextView = itemView.findViewById(R.id.posted)
val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)
val pages: TextView = itemView.findViewById(R.id.pages)
val downloaded: ImageView = itemView.findViewById(R.id.downloaded)
}
private inner class MoveDialogHelper(
@@ -392,7 +373,6 @@ class HistoryScene : ToolbarScene() {
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
mRecyclerView?.outOfCustomChoiceMode()
val downloadManager = EhApplication.getDownloadManager(requireContext())
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
@@ -432,7 +412,7 @@ class HistoryScene : ToolbarScene() {
holder.rating.rating = gi.rating
val category = holder.category
val newCategoryText = EhUtils.getCategory(gi.category)
if (!newCategoryText!!.contentEquals(category.text)) {
if (!newCategoryText.contentEquals(category.text)) {
category.text = newCategoryText
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))
}
@@ -447,16 +427,9 @@ class HistoryScene : ToolbarScene() {
holder.simpleLanguage.visibility = View.VISIBLE
}
holder.downloaded.visibility =
if (mDownloadManager!!.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
// Update transition name
val gid = gi.gid
ViewCompat.setTransitionName(
holder.thumb,
TransitionNameFactory.getThumbTransitionName(gid)
)
holder.card.setOnClickListener { onItemClick(position) }
holder.card.setOnLongClickListener { onItemLongClick(position) }
if (mDownloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
holder.card.setOnClickListener { onItemClick(gi) }
holder.card.setOnLongClickListener { onItemLongClick(gi) }
}
}
@@ -478,12 +451,9 @@ class HistoryScene : ToolbarScene() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val mPosition = viewHolder.bindingAdapterPosition
if (mAdapter == null) {
return
}
val info: HistoryInfo = mAdapter!!.peek(mPosition)!!
EhDB.deleteHistoryInfo(info)
mAdapter!!.refresh()
val info: HistoryInfo? = mAdapter.peek(mPosition)
info?.let { EhDB.deleteHistoryInfo(info) }
mAdapter.refresh()
}
}
}
@@ -45,7 +45,6 @@ import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.WebViewUtil;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.ViewUtils;
@@ -226,10 +225,6 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
} else if (mSignIn == v) {
signIn();
} else if (mSignInViaWebView == v) {
if (!WebViewUtil.available(getContext())) {
showTip(R.string.error_no_webview, BaseScene.LENGTH_SHORT);
return;
}
startScene(new Announcer(WebViewSignInScene.class).setRequestCode(this, REQUEST_CODE_WEBVIEW));
} else if (mSignInViaCookies == v) {
startScene(new Announcer(CookieSignInScene.class).setRequestCode(this, REQUEST_CODE_COOKIE));
@@ -1,124 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import com.hippo.ehviewer.R;
public abstract class ToolbarScene extends BaseScene {
@Nullable
private Toolbar mToolbar;
private CharSequence mTempTitle;
@Nullable
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return null;
}
@Nullable
@Override
public final View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_toolbar, container, false);
Toolbar toolbar = view.findViewById(R.id.toolbar);
FrameLayout contentPanel = view.findViewById(R.id.content_panel);
View contentView = onCreateViewWithToolbar(inflater, contentPanel, savedInstanceState);
if (contentView == null) {
return null;
} else {
mToolbar = toolbar;
contentPanel.addView(contentView, 0);
return view;
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mToolbar = null;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (mToolbar != null) {
if (mTempTitle != null) {
mToolbar.setTitle(mTempTitle);
mTempTitle = null;
}
int menuResId = getMenuResId();
if (menuResId != 0) {
mToolbar.inflateMenu(menuResId);
mToolbar.setOnMenuItemClickListener(ToolbarScene.this::onMenuItemClick);
}
mToolbar.setNavigationOnClickListener(v -> onNavigationClick());
}
}
public int getMenuResId() {
return 0;
}
public boolean onMenuItemClick(MenuItem item) {
return false;
}
public void onNavigationClick() {
}
public void setNavigationIcon(@DrawableRes int resId) {
if (mToolbar != null) {
mToolbar.setNavigationIcon(resId);
}
}
public void setNavigationIcon(@Nullable Drawable icon) {
if (mToolbar != null) {
mToolbar.setNavigationIcon(icon);
}
}
public void setTitle(@StringRes int resId) {
setTitle(getString(resId));
}
public void setTitle(CharSequence title) {
if (mToolbar != null) {
mToolbar.setTitle(title);
} else {
mTempTitle = title;
}
}
}
@@ -0,0 +1,107 @@
/*
* 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.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.core.view.size
import com.google.android.material.appbar.AppBarLayout
import com.hippo.ehviewer.R
abstract class ToolbarScene : BaseScene() {
protected var mToolbar: Toolbar? = null
protected var mAppBarLayout: AppBarLayout? = null
private var mTempTitle: CharSequence? = null
abstract fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.scene_toolbar, container, false) as ViewGroup
mToolbar = view.findViewById(R.id.toolbar)
mAppBarLayout = view.findViewById(R.id.appbar)
val contentView = onCreateViewWithToolbar(inflater, view, savedInstanceState)
return view.apply { addView(contentView, 0) }
}
override fun onDestroyView() {
super.onDestroyView()
mToolbar = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mToolbar?.apply {
mTempTitle?.let { title = it }
val menuResId = getMenuResId()
if (menuResId != 0) {
inflateMenu(menuResId)
setOnMenuItemClickListener { item: MenuItem -> onMenuItemClick(item) }
}
setNavigationOnClickListener { onNavigationClick() }
}
}
fun hideMenu() {
mToolbar?.menu?.clear()
}
fun showMenu(menuResId: Int) {
if (mToolbar?.menu?.size != 0) return
mToolbar?.apply {
inflateMenu(menuResId)
setOnMenuItemClickListener { item: MenuItem -> onMenuItemClick(item) }
}
}
open fun getMenuResId(): Int {
return 0
}
open fun onMenuItemClick(item: MenuItem): Boolean {
return false
}
open fun onNavigationClick() {}
fun setNavigationIcon(@DrawableRes resId: Int) {
mToolbar?.setNavigationIcon(resId)
}
fun setTitle(@StringRes resId: Int) {
setTitle(getString(resId))
}
fun setTitle(title: CharSequence?) {
mToolbar?.title = title
mTempTitle = title
}
fun setLiftOnScrollTargetView(view: View?) {
mAppBarLayout?.setLiftOnScrollTargetView(view)
}
}
@@ -20,7 +20,12 @@ import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.*
import android.widget.AutoCompleteTextView
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
import com.hippo.yorozuya.NumberUtils
@@ -28,14 +33,8 @@ 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 mSto: CheckBox
private var mSr: CheckBox
private var mMinRating: TextInputLayout
private var mSp: CheckBox
@@ -51,28 +50,19 @@ class AdvanceSearchTable @JvmOverloads constructor(
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
mSh = row0.getChildAt(0) as CheckBox
mSto = row0.getChildAt(1) as CheckBox
val row1 = getChildAt(1) as ViewGroup
mSdesc = row1.getChildAt(0) as CheckBox
mStorr = row1.getChildAt(1) as CheckBox
mSr = row1.getChildAt(0) as CheckBox
mMinRating = row1.getChildAt(1) as TextInputLayout
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
mSp = row2.getChildAt(0) as CheckBox
mSpf = row2.getChildAt(1) as EditText
mSpt = row2.getChildAt(3) as EditText
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
mSfl = row4.getChildAt(0) as CheckBox
mSfu = row4.getChildAt(1) as CheckBox
mSft = row4.getChildAt(2) as CheckBox
mSpt.setOnEditorActionListener { v: TextView, _: Int, _: KeyEvent? ->
val nextView = v.focusSearch(
@@ -98,31 +88,16 @@ class AdvanceSearchTable @JvmOverloads constructor(
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 (mSto.isChecked) advanceSearch = advanceSearch or STO
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)
mSto.isChecked = NumberUtils.int2boolean(advanceSearch and STO)
mSfl.isChecked = NumberUtils.int2boolean(advanceSearch and SFL)
mSfu.isChecked = NumberUtils.int2boolean(advanceSearch and SFU)
mSft.isChecked = NumberUtils.int2boolean(advanceSearch and SFT)
@@ -174,14 +149,8 @@ class AdvanceSearchTable @JvmOverloads constructor(
}
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 SH = 0x1
const val STO = 0x2
const val SFL = 0x100
const val SFU = 0x200
const val SFT = 0x400
@@ -1,128 +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.widget;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Parcelable;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.FavouriteStatusRouter;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.widget.ContentLayout;
import com.hippo.yorozuya.IntIdGenerator;
import java.util.HashMap;
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<>();
public GalleryInfoContentHelper() {
listener = (gid, slot) -> {
GalleryInfo info = map.get(gid);
if (info != null) {
info.favoriteSlot = slot;
}
};
EhApplication.getFavouriteStatusRouter().addListener(listener);
}
public void destroy() {
EhApplication.getFavouriteStatusRouter().removeListener(listener);
}
@Override
protected void onAddData(GalleryInfo data) {
map.put(data.gid, data);
}
@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);
}
}
@Override
protected void onRemoveData(GalleryInfo data) {
map.remove(data.gid);
}
@Override
protected void onRemoveData(List<GalleryInfo> data) {
for (GalleryInfo info : data) {
map.remove(info.gid);
}
}
@Override
protected void onClearData() {
map.clear();
maxGid = -1;
minGid = -1;
}
@Override
protected Parcelable saveInstanceState(Parcelable superState) {
Bundle bundle = (Bundle) super.saveInstanceState(superState);
// TODO It's a bad design
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;
}
@Override
protected Parcelable restoreInstanceState(Parcelable state) {
Bundle bundle = (Bundle) state;
int id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID);
if (id != IntIdGenerator.INVALID_ID) {
FavouriteStatusRouter router = EhApplication.getFavouriteStatusRouter();
Map<Long, GalleryInfo> map = router.restoreDataMap(id);
if (map != null) {
this.map = map;
}
}
minGid = bundle.getInt(KEY_MIN_GID);
maxGid = bundle.getInt(KEY_MAX_GID);
return super.restoreInstanceState(state);
}
}
@@ -0,0 +1,145 @@
/*
* 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.widget
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.FavouriteStatusRouter
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.widget.ContentLayout.ContentHelper
import com.hippo.yorozuya.IntIdGenerator
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
@JvmField
var maxGid = -1
@JvmField
var minGid = -1
@JvmField
var jumpTo: String? = null
private val listener: FavouriteStatusRouter.Listener
@SuppressLint("UseSparseArrays")
private var map: MutableMap<Long, GalleryInfo> = HashMap()
init {
listener = FavouriteStatusRouter.Listener { gid: Long, slot: Int ->
val info = map[gid]
if (info != null) {
info.favoriteSlot = slot
}
}
EhApplication.getFavouriteStatusRouter().addListener(listener)
}
fun destroy() {
EhApplication.getFavouriteStatusRouter().removeListener(listener)
}
override fun onAddData(data: GalleryInfo?) {
data?.let { map[data.gid] = data }
}
override fun onAddData(data: List<GalleryInfo?>) {
for (info in data) {
info?.let {
if (maxGid == -1) maxGid = info.gid.toInt()
if (minGid == -1) minGid = info.gid.toInt()
maxGid = max(maxGid.toLong(), info.gid).toInt()
minGid = min(minGid.toLong(), info.gid).toInt()
map[info.gid] = info
}
}
}
override fun onRemoveData(data: GalleryInfo?) {
data?.let { map.remove(data.gid) }
}
override fun onRemoveData(data: List<GalleryInfo?>) {
for (info in data) {
info?.let { map.remove(info.gid) }
}
}
override fun onClearData() {
map.clear()
maxGid = -1
minGid = -1
}
override fun beforeRefresh() {
super.beforeRefresh()
maxGid = -1
minGid = -1
}
override fun saveInstanceState(superState: Parcelable): Parcelable {
val bundle = super.saveInstanceState(superState) as Bundle
// TODO It's a bad design
val router = EhApplication.getFavouriteStatusRouter()
val id = router.saveDataMap(map)
bundle.putInt(KEY_DATA_MAP, id)
bundle.putInt(KEY_MIN_GID, minGid)
bundle.putInt(KEY_MAX_GID, maxGid)
return bundle
}
override fun restoreInstanceState(state: Parcelable): Parcelable {
val bundle = state as Bundle
val id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID)
if (id != IntIdGenerator.INVALID_ID) {
val router = EhApplication.getFavouriteStatusRouter()
val map = router.restoreDataMap(id)
if (map != null) {
this.map = map
}
}
minGid = bundle.getInt(KEY_MIN_GID)
maxGid = bundle.getInt(KEY_MAX_GID)
return super.restoreInstanceState(state)
}
fun goTo(time: Long) {
val formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
jumpTo = formatter.format(Instant.ofEpochMilli(time))
doGoToPage(Int.MAX_VALUE / 2)
}
fun goToPage(page: Int) {
jumpTo = page.toString()
doGoToPage(page)
}
companion object {
private const val KEY_DATA_MAP = "data_map"
private const val KEY_MAX_GID = "max_gid"
private const val KEY_MIN_GID = "min_gid"
const val KEY_UPPER_PAGE = "upper_page"
const val KEY_LOWER_PAGE = "lower_page"
}
}
@@ -58,7 +58,6 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
private View mSelectImage;
private CheckBox mSearchUSS;
private CheckBox mSearchOSC;
private CheckBox mSearchSE;
private Helper mHelper;
@@ -93,7 +92,6 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
mSelectImage = ViewUtils.$$(this, R.id.select_image);
mSearchUSS = (CheckBox) ViewUtils.$$(this, R.id.search_uss);
mSearchOSC = (CheckBox) ViewUtils.$$(this, R.id.search_osc);
mSearchSE = (CheckBox) ViewUtils.$$(this, R.id.search_se);
mSelectImage.setOnClickListener(this);
}
@@ -182,7 +180,6 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
builder.setImagePath(mImagePath);
builder.setUseSimilarityScan(mSearchUSS.isChecked());
builder.setOnlySearchCovers(mSearchOSC.isChecked());
builder.setShowExpunged(mSearchSE.isChecked());
}
@Override
@@ -476,8 +476,7 @@ public class SearchBar extends MaterialCardView implements View.OnClickListener,
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle savedState = (Bundle) state;
if (state instanceof final Bundle savedState) {
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setState(savedState.getInt(STATE_KEY_STATE), false);
}
@@ -113,7 +113,7 @@ public final class SearchDatabase {
*
* @param maxEntries Max entries to leave in the table. 0 means remove all entries.
*/
protected void truncateHistory(int maxEntries) {
private void truncateHistory(int maxEntries) {
if (maxEntries < 0) {
throw new IllegalArgumentException();
}
@@ -28,6 +28,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.IntDef
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DefaultItemAnimator
@@ -187,12 +188,15 @@ class SearchLayout @JvmOverloads constructor(
R.id.search_subscription_search -> {
urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
}
R.id.search_specify_uploader -> {
urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
}
R.id.search_specify_tag -> {
urlBuilder.mode = ListUrlBuilder.MODE_TAG
}
else -> {
urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
}
@@ -208,6 +212,7 @@ class SearchLayout @JvmOverloads constructor(
urlBuilder.pageTo = mTableAdvanceSearch.pageTo
}
}
SEARCH_MODE_IMAGE -> {
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
mImageView.formatListUrlBuilder(urlBuilder)
@@ -242,7 +247,7 @@ class SearchLayout @JvmOverloads constructor(
override fun onTabReselected(tab: TabLayout.Tab) {}
@IntDef(SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Retention(AnnotationRetention.SOURCE)
private annotation class SearchMode
interface Helper {
fun onChangeSearchMode()
@@ -291,11 +296,13 @@ class SearchLayout @JvmOverloads constructor(
ViewUtils.removeFromParent(mNormalView)
content.addView(mNormalView)
}
ITEM_TYPE_NORMAL_ADVANCE -> {
title.setText(R.string.search_advance)
ViewUtils.removeFromParent(mAdvanceView)
content.addView(mAdvanceView)
}
ITEM_TYPE_IMAGE -> {
title.setText(R.string.search_image)
ViewUtils.removeFromParent(mImageView)
@@ -323,7 +330,7 @@ class SearchLayout @JvmOverloads constructor(
inner class IdentifiedChip @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Chip(context, attrs, R.attr.filterChipStyle) {
) : Chip(ContextThemeWrapper(context, com.google.android.material.R.style.Widget_Material3_Chip_Filter), attrs) {
var idt = 0
}
@@ -57,7 +57,6 @@ public abstract class GalleryProvider {
public void stop() {
OSUtils.checkMainLoop();
mImageCache.evictAll();
Image.lazyEvictAll();
}
public void setGLRoot(GLRoot glRoot) {
@@ -246,7 +245,7 @@ public abstract class GalleryProvider {
}
public void add(Integer key, ImageWrapper value) {
if (value.getFormat() != Image.FORMAT_ANIMATED)
if (!value.getAnimated())
if (value.obtain())
put(key, value);
}
@@ -983,15 +983,13 @@ public final class GalleryView extends GLView implements GestureRecognizer.Liste
private final Context mContext;
private final Adapter mAdapter;
private final int mEdgeColor = Color.WHITE;
private Listener mListener;
private int mLayoutMode = LAYOUT_LEFT_TO_RIGHT;
private int mScaleMode = SCALE_FIT;
private int mStartPosition = START_POSITION_TOP_LEFT;
private int mStartPage = 0;
private int mBackgroundColor = Color.BLACK;
private int mEdgeColor = Color.WHITE;
private int mPagerInterval = 48;
private int mScrollInterval = 24;
private int mPageMinHeight = 256;
@@ -62,8 +62,8 @@ public class GLES11Canvas implements GLCanvas {
private static final int MSKEW_Y = 1;
private static final int MSCALE_X = 0;
private static final int MSCALE_Y = 5;
private static float[] sCropRect = new float[4];
private static GLId mGLId = new GLES11IdImpl();
private static final float[] sCropRect = new float[4];
private static final GLId mGLId = new GLES11IdImpl();
static {
float[] temp = {
@@ -87,23 +87,23 @@ public class GLES11Canvas implements GLCanvas {
float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2;
float positive = value + 0.5f;
float negative = -value + 0.5f;
BOX_COORDINATES[arrayOffset + n * 0 + i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 2 - i * 2 + 0] = positive;
BOX_COORDINATES[arrayOffset + n * 2 + i * 2 + 0] = negative;
BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive;
BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative;
BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative;
BOX_COORDINATES[arrayOffset + n * 6 - i * 2 + 0] = negative;
BOX_COORDINATES[arrayOffset + n * 6 + i * 2 + 0] = positive;
BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative;
BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive;
BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative;
}
}
private final float mMatrixValues[] = new float[16];
private final float mTextureMatrixValues[] = new float[16];
private final float[] mMatrixValues = new float[16];
private final float[] mTextureMatrixValues = new float[16];
// The results of mapPoints are stored in this buffer, and the order is
// x1, y1, x2, y2.
private final float mMapPointsBuffer[] = new float[4];
private final float mTextureColor[] = new float[4];
private final float[] mMapPointsBuffer = new float[4];
private final float[] mTextureColor = new float[4];
private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
private final RectF mDrawTextureSourceRect = new RectF();
@@ -111,21 +111,21 @@ public class GLES11Canvas implements GLCanvas {
private final float[] mTempMatrix = new float[32];
private final IntList mUnboundTextures = new IntList();
private final IntList mDeleteBuffers = new IntList();
private final GL11 mGL;
private final int mBoxCoords;
private final GLState mGLState;
private final boolean mBlendEnabled = true;
private final int[] mFrameBuffer = new int[1];
// Drawing statistics
int mCountDrawLine;
int mCountFillRect;
int mCountDrawMesh;
int mCountTextureRect;
int mCountTextureOES;
private GL11 mGL;
private int mBoxCoords;
private GLState mGLState;
private float mAlpha;
private ConfigState mRecycledRestoreAction;
private int mScreenWidth;
private int mScreenHeight;
private boolean mBlendEnabled = true;
private int mFrameBuffer[] = new int[1];
private RawTexture mTargetTexture;
public GLES11Canvas(GL11 gl) {
@@ -191,7 +191,7 @@ public class GLES11Canvas implements GLCanvas {
}
}
private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
private static boolean isMatrixRotatedOrFlipped(float[] matrix) {
final float eps = 1e-5f;
return Math.abs(matrix[MSKEW_X]) > eps
|| Math.abs(matrix[MSKEW_Y]) > eps
@@ -247,7 +247,7 @@ public class GLES11Canvas implements GLCanvas {
gl.glMatrixMode(GL11.GL_MODELVIEW);
gl.glLoadIdentity();
float matrix[] = mMatrixValues;
float[] matrix = mMatrixValues;
Matrix.setIdentityM(matrix, 0);
// to match the graphic coordinate system in android, we flip it vertically.
if (mTargetTexture == null) {
@@ -408,7 +408,7 @@ public class GLES11Canvas implements GLCanvas {
}
@Override
public void multiplyMatrix(float matrix[], int offset) {
public void multiplyMatrix(float[] matrix, int offset) {
float[] temp = mTempMatrix;
Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
System.arraycopy(temp, 0, mMatrixValues, 0, 16);
@@ -467,7 +467,7 @@ public class GLES11Canvas implements GLCanvas {
// Transforms two points by the given matrix m. The result
// {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
private float[] mapPoints(float[] m, int x1, int y1, int x2, int y2) {
float[] r = mMapPointsBuffer;
// Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
@@ -506,7 +506,7 @@ public class GLES11Canvas implements GLCanvas {
textureRect(x, y, width, height);
} else {
// draw the rect from bottom-left to top-right
float points[] = mapPoints(
float[] points = mapPoints(
mMatrixValues, x, y + height, x + width, y);
x = (int) (points[0] + 0.5f);
y = (int) (points[1] + 0.5f);
@@ -958,13 +958,12 @@ public class GLES11Canvas implements GLCanvas {
private static class GLState {
private final GL11 mGL;
private final boolean mLineSmooth = false;
private int mTexEnvMode = GL11.GL_REPLACE;
private float mTextureAlpha = 1.0f;
private int mTextureTarget = GL11.GL_TEXTURE_2D;
private boolean mBlendEnabled = true;
private float mLineWidth = 1.0f;
private boolean mLineSmooth = false;
public GLState(GL11 gl) {
mGL = gl;
@@ -1059,7 +1058,7 @@ public class GLES11Canvas implements GLCanvas {
private static class ConfigState {
float mAlpha;
float mMatrix[] = new float[16];
float[] mMatrix = new float[16];
ConfigState mNextFree;
public void restore(GLES11Canvas canvas) {
@@ -146,13 +146,13 @@ public class GLES20Canvas implements GLCanvas {
float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2;
float positive = value + 0.5f;
float negative = -value + 0.5f;
BOX_COORDINATES[arrayOffset + n * 0 + i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 2 - i * 2 + 0] = positive;
BOX_COORDINATES[arrayOffset + n * 2 + i * 2 + 0] = negative;
BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive;
BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative;
BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive;
BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative;
BOX_COORDINATES[arrayOffset + n * 6 - i * 2 + 0] = negative;
BOX_COORDINATES[arrayOffset + n * 6 + i * 2 + 0] = positive;
BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative;
BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive;
BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative;
}
}
@@ -433,7 +433,7 @@ public class GLES20Canvas implements GLCanvas {
public void translate(float x, float y) {
int index = mCurrentMatrixIndex;
float[] m = mMatrices;
m[index + 12] += m[index + 0] * x + m[index + 4] * y;
m[index + 12] += m[index] * x + m[index + 4] * y;
m[index + 13] += m[index + 1] * x + m[index + 5] * y;
m[index + 14] += m[index + 2] * x + m[index + 6] * y;
m[index + 15] += m[index + 3] * x + m[index + 7] * y;
@@ -96,7 +96,7 @@ public final class MovableTextTexture extends SpriteTexture {
int y = 0;
for (int i = 0; i < length; i++) {
int offset = i * 4;
rects[offset + 0] = x;
rects[offset] = x;
rects[offset + 1] = y;
rects[offset + 2] = (int) widths[i];
rects[offset + 3] = height;
@@ -30,9 +30,9 @@ class NinePatchChunk {
public Rect mPaddings = new Rect();
public int mDivX[];
public int mDivY[];
public int mColor[];
public int[] mDivX;
public int[] mDivY;
public int[] mColor;
private static void readIntArray(int[] data, ByteBuffer buffer) {
for (int i = 0, n = data.length; i < n; ++i) {
@@ -35,9 +35,9 @@ import java.nio.FloatBuffer;
public class NinePatchTexture extends ResourceTexture {
@SuppressWarnings("unused")
private static final String TAG = "NinePatchTexture";
private NinePatchChunk mChunk;
private SmallCache<NinePatchInstance> mInstanceCache
private final SmallCache<NinePatchInstance> mInstanceCache
= new SmallCache<NinePatchInstance>();
private NinePatchChunk mChunk;
public NinePatchTexture(Context context, int resId) {
super(context, resId);
@@ -120,8 +120,8 @@ public class NinePatchTexture extends ResourceTexture {
private static class SmallCache<V> {
private static final int CACHE_SIZE = 16;
private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
private int[] mKey = new int[CACHE_SIZE];
private V[] mValue = (V[]) new Object[CACHE_SIZE];
private final int[] mKey = new int[CACHE_SIZE];
private final V[] mValue = (V[]) new Object[CACHE_SIZE];
private int mCount; // number of items in this cache
// Puts a value into the cache. If the cache is full, also returns
@@ -217,10 +217,10 @@ class NinePatchInstance {
throw new RuntimeException("unsupported nine patch");
}
float divX[] = new float[4];
float divY[] = new float[4];
float divU[] = new float[4];
float divV[] = new float[4];
float[] divX = new float[4];
float[] divY = new float[4];
float[] divU = new float[4];
float[] divV = new float[4];
int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
@@ -261,7 +261,7 @@ class NinePatchInstance {
* @return the number of these dividers.
*/
private static int stretch(
float x[], float u[], int div[], int source, int target) {
float[] x, float[] u, int[] div, int source, int target) {
int textureSize = MathUtils.nextPowerOf2(source);
float textureBound = (float) source / textureSize;
@@ -313,7 +313,7 @@ class NinePatchInstance {
return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
private void prepareVertexData(float x[], float y[], float u[], float v[],
private void prepareVertexData(float[] x, float[] y, float[] u, float[] v,
int nx, int ny, int[] color) {
/*
* Given a 3x3 nine-patch image, the vertex order is defined as the
@@ -335,8 +335,8 @@ class NinePatchInstance {
* index: 04152637B6A5948C9DAEBF
*/
int pntCount = 0;
float xy[] = new float[VERTEX_BUFFER_SIZE];
float uv[] = new float[VERTEX_BUFFER_SIZE];
float[] xy = new float[VERTEX_BUFFER_SIZE];
float[] uv = new float[VERTEX_BUFFER_SIZE];
for (int j = 0; j < ny; ++j) {
for (int i = 0; i < nx; ++i) {
int xIndex = (pntCount++) << 1;
@@ -350,7 +350,7 @@ class NinePatchInstance {
int idxCount = 1;
boolean isForward = false;
byte index[] = new byte[INDEX_BUFFER_SIZE];
byte[] index = new byte[INDEX_BUFFER_SIZE];
for (int row = 0; row < ny - 1; row++) {
--idxCount;
isForward = !isForward;
@@ -279,8 +279,7 @@ public abstract class UploadedTexture extends BasicTexture {
@Override
public boolean equals(Object object) {
if (!(object instanceof BorderKey)) return false;
BorderKey o = (BorderKey) object;
if (!(object instanceof BorderKey o)) return false;
return vertical == o.vertical
&& config == o.config && length == o.length;
}
@@ -95,7 +95,7 @@ public class ImageMovableTextTexture extends ImageSpriteTexture {
int y = 0;
for (int i = 0; i < length; i++) {
int offset = i * 4;
rects[offset + 0] = x;
rects[offset] = x;
rects[offset + 1] = y;
rects[offset + 2] = (int) widths[i];
rects[offset + 3] = height;
@@ -112,7 +112,6 @@ public class ImageMovableTextTexture extends ImageSpriteTexture {
}
Image image = Image.create(bitmap);
bitmap.recycle();
if (image == null) {
return null;
}
@@ -233,7 +233,7 @@ public class ImageTexture implements Texture, Animatable {
}
boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() ||
(mImage.getFormat() == 0) || mRunning.get();
(!mImage.getAnimated()) || mRunning.get();
synchronized (mImage) {
mImageBusy = false;
@@ -595,7 +595,7 @@ public class ImageTexture implements Texture, Animatable {
// Release image
mImageBusy = false;
// Check need release, frameCount <= 1
if (mNeedRelease.get() || mImage.getFormat() == 0) {
if (mNeedRelease.get() || !mImage.getAnimated()) {
mAnimateRunnable = null;
return;
}
@@ -617,7 +617,7 @@ public class ImageTexture implements Texture, Animatable {
mImageBusy = true;
}
mImage.advance();
mImage.start();
long delay = mImage.getDelay();
long time = System.nanoTime();
if (-1L != lastDelay) {
@@ -102,14 +102,14 @@ public class ImageWrapper {
}
/**
* @see Image#getFormat()
* @see Image#getAnimated()
*/
public int getFormat() {
return mImage.getFormat();
public Boolean getAnimated() {
return mImage.getAnimated();
}
/**
* @see Image#getFormat()
* @see Image#getAnimated()
*/
public int getWidth() {
return mCut.width();
@@ -123,12 +123,12 @@ public class ImageWrapper {
}
/**
* @see Image#render(int, int, Bitmap, int, int, int, int, boolean, int)
* @see Image#render(int, int, Bitmap, int, int, int, int)
*/
public void render(int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height, boolean fillBlank, int defaultColor) {
mImage.render(srcX + mCut.left, srcY + mCut.top, dst, dstX, dstY,
width, height, fillBlank, defaultColor);
width, height);
}
/**
@@ -139,10 +139,10 @@ public class ImageWrapper {
}
/**
* @see Image#advance()
* @see Image#start()
*/
public void advance() {
mImage.advance();
public void start() {
mImage.start();
}
/**
@@ -54,21 +54,17 @@ public class GLProgressView extends GLView {
}
private final GLPaint mGLPaint;
private final List<Animation> mAnimations;
private float mCx;
private float mCy;
private float mRadiusX;
private float mRadiusY;
private float mTrimStart = 0.0f;
private float mTrimEnd = 0.0f;
private float mTrimOffset = 0.0f;
private float mTrimRotation = 0.0f;
private boolean mIndeterminate = false;
private List<Animation> mAnimations;
public GLProgressView() {
mGLPaint = new GLPaint();
mGLPaint.setColor(Color.WHITE);
+173 -221
View File
@@ -1,267 +1,219 @@
/*
* Copyright 2016 Hippo Seven
* 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
* This file is part of EhViewer
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* 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.image
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import com.hippo.Native.getFd
import java.io.FileDescriptor
import java.util.function.Consumer
import android.graphics.Canvas
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.ALLOCATOR_DEFAULT
import android.graphics.ImageDecoder.ALLOCATOR_SOFTWARE
import android.graphics.ImageDecoder.DecodeException
import android.graphics.ImageDecoder.ImageInfo
import android.graphics.ImageDecoder.Source
import android.graphics.PixelFormat
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toDrawable
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.jvm.Throws
import kotlin.math.max
/**
* The `Image` is a image which stored pixel data in native heap
*/
class Image private constructor(nativePtr: Long) {
private var mNativePtr: Long
private var needRecycle = false
private var nativeProcessCnt = 0
class Image private constructor(
source: Source?, drawable: Drawable? = null,
val hardware: Boolean = true,
val release: () -> Unit? = {}
) {
internal var mObtainedDrawable: Drawable?
private var mBitmap: Bitmap? = null
init {
if (needEvictAll) evictAll()
mNativePtr = nativePtr
imageList.add(this)
}
mObtainedDrawable = null
source?.let {
mObtainedDrawable =
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, src: Source ->
decoder.allocator = if (hardware) ALLOCATOR_DEFAULT else ALLOCATOR_SOFTWARE
// Sadly we must use software memory since we need copy it to tile buffer, fuck glgallery
// Idk it will cause how much performance regression
/**
* Return the format of the image
*/
val format: Int
get() {
checkRecycledAndLock()
val ret = nativeGetFormat(mNativePtr)
releaseNativeLock()
return ret
decoder.setTargetSampleSize(
max(
max(
info.size.width / (2 * screenWidth),
info.size.height / (2 * screenHeight)
), 1
)
)
// Don't
} // Should we lazy decode it?
}
/**
* Return the width of the image
*/
val width: Int
get() {
checkRecycledAndLock()
val ret = nativeGetWidth(mNativePtr)
releaseNativeLock()
return ret
if (mObtainedDrawable == null) {
mObtainedDrawable = drawable!!
}
/**
* Return the height of the image
*/
val height: Int
get() {
checkRecycledAndLock()
val ret = nativeGetHeight(mNativePtr)
releaseNativeLock()
return ret
}
@Synchronized
private fun checkRecycledAndLock() {
check(mNativePtr != 0L) { "The image is recycled." }
nativeProcessCnt++
}
@Synchronized
private fun releaseNativeLock() {
nativeProcessCnt--
if (needRecycle) recycle()
}
val animated = mObtainedDrawable is AnimatedImageDrawable
val width = mObtainedDrawable!!.intrinsicWidth
val height = mObtainedDrawable!!.intrinsicHeight
val isRecycled = mObtainedDrawable == null
/**
* Render the image to `Bitmap`
*/
fun render(
srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int,
width: Int, height: Int, fillBlank: Boolean, defaultColor: Int
) {
checkRecycledAndLock()
nativeRender(mNativePtr, srcX, srcY, dst, dstX, dstY, width, height)
releaseNativeLock()
}
var started = false
/**
* Call `glTexImage2D` for init is true and
* call `glTexSubImage2D` for init is false.
* width * height must <= 512 * 512 or do nothing
*/
fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) {
checkRecycledAndLock()
nativeTexImage(mNativePtr, init, offsetX, offsetY, width, height)
releaseNativeLock()
}
/**
* Move to next frame. Do nothing for non-animation image
*/
fun advance() {
checkRecycledAndLock()
nativeAdvance(mNativePtr)
releaseNativeLock()
}
/**
* Return current frame delay. 0 for non-animation image
*/
val delay: Int
get() {
checkRecycledAndLock()
val delay = nativeGetDelay(mNativePtr)
releaseNativeLock()
return if (delay <= 10) 100 else delay
}
/**
* Return is the image opaque
*/
val isOpaque: Boolean
get() {
checkRecycledAndLock()
val ret = nativeIsOpaque(mNativePtr)
releaseNativeLock()
return ret
}
/**
* Free the native object associated with this image.
* It must be called when the image will not be used.
* The image can't be used after this method is called.
*/
@Synchronized
fun recycle() {
if (mNativePtr != 0L) {
if (nativeProcessCnt != 0) {
needRecycle = true
return
}
nativeRecycle(mNativePtr)
mNativePtr = 0
imageList.remove(this)
if (mObtainedDrawable is AnimatedImageDrawable) {
(mObtainedDrawable as AnimatedImageDrawable?)?.stop()
}
if (mObtainedDrawable is BitmapDrawable) {
(mObtainedDrawable as BitmapDrawable?)?.bitmap?.recycle()
}
mObtainedDrawable?.callback = null
mObtainedDrawable = null
mBitmap?.recycle()
mBitmap = null
release()
}
private fun prepareBitmap() {
if (mBitmap != null) return
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}
private fun updateBitmap() {
prepareBitmap()
mObtainedDrawable!!.draw(Canvas(mBitmap!!))
}
fun render(
srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int,
width: Int, height: Int
) {
check(!hardware) { "Hardware buffer cannot be used in glgallery" }
val bitmap: Bitmap = if (animated) {
updateBitmap()
mBitmap!!
} else {
(mObtainedDrawable as BitmapDrawable).bitmap
}
nativeRender(
bitmap,
srcX,
srcY,
dst,
dstX,
dstY,
width,
height
)
}
fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) {
check(!hardware) { "Hardware buffer cannot be used in glgallery" }
val bitmap: Bitmap? = if (animated) {
updateBitmap()
mBitmap
} else {
(mObtainedDrawable as BitmapDrawable?)?.bitmap
}
bitmap ?: return
nativeTexImage(
bitmap,
init,
offsetX,
offsetY,
width,
height
)
}
fun start() {
if (!started) {
(mObtainedDrawable as AnimatedImageDrawable?)?.start()
}
}
/**
* Returns true if this image has been recycled.
*/
val isRecycled: Boolean
get() = mNativePtr == 0L
val delay: Int
get() {
if (animated)
return 100
return 0
}
val isOpaque: Boolean
get() {
return mObtainedDrawable?.opacity == PixelFormat.OPAQUE
}
companion object {
const val FORMAT_NORMAL = 0
const val FORMAT_ANIMATED = 1
private val imageList = ArrayList<Image>()
private var needEvictAll = false
private fun newFromAddr(addr: Long): Image? {
return if (addr == 0L) null else Image(addr)
var screenWidth: Int = 0
var screenHeight: Int = 0
@JvmStatic
fun initialize(context: Context) {
screenWidth = context.resources.displayMetrics.widthPixels
screenHeight = context.resources.displayMetrics.heightPixels
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream, hardware: Boolean = true): Image {
val src = ImageDecoder.createSource(
stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
)
return Image(src, hardware = hardware)
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer, hardware: Boolean = true, release: () -> Unit? = {}): Image {
val src = ImageDecoder.createSource(buffer)
return Image(src, hardware = hardware) {
release()
}
}
@JvmStatic
fun lazyEvictAll() {
needEvictAll = true
fun create(bitmap: Bitmap): Image {
return Image(null, bitmap.toDrawable(Resources.getSystem()), false)
}
private fun evictAll() {
ArrayList(imageList).forEach(
Consumer { obj: Image -> obj.recycle() })
needEvictAll = false
}
/**
* Decode image from `InputStream`
*/
@JvmStatic
fun decode(fd: FileDescriptor?, partially: Boolean): Image? {
return newFromAddr(nativeDecode(getFd(fd)))
}
/**
* Decode image from `InputStream`
*/
@JvmStatic
fun decode(fd: Int, partially: Boolean): Image? {
return newFromAddr(nativeDecode(fd))
}
/**
* Decode image from `InputStream`
*/
@JvmStatic
fun decodeAddr(addr: Long, partially: Boolean): Image? {
return newFromAddr(nativeDecodeAddr(addr))
}
/**
* Create a plain image from Bitmap
*/
@JvmStatic
fun create(bitmap: Bitmap): Image? {
return newFromAddr(nativeCreate(bitmap))
}
/**
* Return all un-recycled `Image` instance count.
* It is useful for debug.
*/
@JvmStatic
val imageCount: Int
get() = imageList.size
@JvmStatic
private external fun nativeDecode(fd: Int): Long
@JvmStatic
private external fun nativeDecodeAddr(addr: Long): Long
@JvmStatic
private external fun nativeCreate(bitmap: Bitmap): Long
@JvmStatic
private external fun nativeRender(
nativePtr: Long,
bitmap: Bitmap,
srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int,
width: Int, height: Int
)
@JvmStatic
private external fun nativeTexImage(
nativePtr: Long,
bitmap: Bitmap,
init: Boolean,
offsetX: Int,
offsetY: Int,
width: Int,
height: Int
)
@JvmStatic
private external fun nativeAdvance(nativePtr: Long)
@JvmStatic
private external fun nativeGetDelay(nativePtr: Long): Int
@JvmStatic
private external fun nativeIsOpaque(nativePtr: Long): Boolean
@JvmStatic
private external fun nativeRecycle(nativePtr: Long)
@JvmStatic
private external fun nativeGetFormat(nativePtr: Long): Int
@JvmStatic
private external fun nativeGetWidth(nativePtr: Long): Int
@JvmStatic
private external fun nativeGetHeight(nativePtr: Long): Int
}
}
+31 -267
View File
@@ -1,82 +1,34 @@
/*
* Copyright 2016 Hippo Seven
* 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
* This file is part of EhViewer
*
* http://www.apache.org/licenses/LICENSE-2.0
* 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.
*
* 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.
* 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.image
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Animatable
import android.os.Handler
import android.os.Looper
import java.io.FileDescriptor
import java.lang.ref.WeakReference
import android.graphics.ImageDecoder
import android.graphics.drawable.Drawable
import java.io.FileInputStream
import kotlin.jvm.Throws
/**
* A image with [Image] for data and [Bitmap] for render.
*/
class ImageBitmap : Animatable, Runnable {
private val mBitmap: Bitmap
/**
* Return the format of the image
*/
val format: Int
/**
* Return image is opaque
*/
val isOpaque: Boolean
private val mCallbackSet: MutableSet<WeakReference<Callback>> = LinkedHashSet()
private var mImage: Image? = null
class ImageBitmap private constructor(image: Image) {
private var mImage: Image? = image
private var mReferences = 0
private var mAnimationReferences = 0
private var mRunning = false
private constructor(image: Image) {
val width = image.width
val height = image.height
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
image.render(0, 0, mBitmap, 0, 0, width, height, false, 0)
format = image.format
isOpaque = image.isOpaque
if (format == 1) {
// For animated image, save image object
mImage = image
} else {
// Free the image
image.recycle()
}
}
private constructor(bitmap: Bitmap) {
format = Image.FORMAT_NORMAL
mBitmap = bitmap
isOpaque = !bitmap.hasAlpha()
}
/**
* Obtain the image bitmap
*
* @return false for the image is recycled and obtain failed
*/
@Synchronized
fun obtain(): Boolean {
return if (mBitmap.isRecycled) {
return if (mImage?.isRecycled == true) {
false
} else {
++mReferences
@@ -84,220 +36,32 @@ class ImageBitmap : Animatable, Runnable {
}
}
/**
* Release the image bitmap
*/
@Synchronized
fun release() {
--mReferences
if (mReferences <= 0 && !mBitmap.isRecycled) {
mBitmap.recycle()
if (mImage != null) {
mImage!!.recycle()
}
if (mReferences <= 0 && !mImage!!.isRecycled) {
mImage!!.recycle()
mImage = null
}
}
/**
* Add a callback for invalidating
*/
fun addCallback(callback: Callback) {
val iterator = mCallbackSet.iterator()
var c: Callback?
while (iterator.hasNext()) {
c = iterator.next().get()
if (c == null) {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove()
} else if (c === callback) {
return
}
}
mCallbackSet.add(WeakReference(callback))
fun getDrawable(): Drawable {
check(obtain()) { "Recycled!" }
return mImage!!.mObtainedDrawable as Drawable
}
/**
* Remove a callback
*/
fun removeCallback(callback: Callback) {
val iterator = mCallbackSet.iterator()
var c: Callback?
while (iterator.hasNext()) {
c = iterator.next().get()
if (c == null) {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove()
} else if (c === callback) {
iterator.remove()
return
}
}
}
/**
* Return image width
*/
val width: Int
get() = mBitmap.width
get() = mImage!!.width
/**
* Return image height
*/
val height: Int
get() = mBitmap.height
/**
* Return image is animated
*/
val isAnimated: Boolean
get() = mImage != null
/**
* Draw image to canvas
*/
fun draw(canvas: Canvas, left: Float, top: Float, paint: Paint?) {
if (!mBitmap.isRecycled) {
canvas.drawBitmap(mBitmap, left, top, paint)
}
}
/**
* Draw image to canvas
*/
fun draw(canvas: Canvas, src: Rect?, dst: Rect, paint: Paint?) {
if (!mBitmap.isRecycled) {
canvas.drawBitmap(mBitmap, src, dst, paint)
}
}
/**
* Draw image to canvas
*/
fun draw(canvas: Canvas, src: Rect?, dst: RectF, paint: Paint?) {
if (!mBitmap.isRecycled) {
canvas.drawBitmap(mBitmap, src, dst, paint)
}
}
/**
* `start()` and `stop()` is a pair
*/
override fun start() {
mAnimationReferences++
if (mBitmap.isRecycled || mImage == null || mRunning) {
return
}
mRunning = true
HANDLER.postDelayed(this, Math.max(0, mImage!!.delay).toLong())
}
/**
* `start()` and `stop()` is a pair
*/
override fun stop() {
mAnimationReferences--
if (mAnimationReferences <= 0) {
mRunning = false
HANDLER.removeCallbacks(this)
}
}
override fun isRunning(): Boolean {
return mRunning
}
private fun notifyUpdate(): Boolean {
var hasCallback = false
val iterator = mCallbackSet.iterator()
var callback: Callback?
while (iterator.hasNext()) {
callback = iterator.next().get()
if (callback != null) {
// Render bitmap int the first time
if (!hasCallback) {
hasCallback = true
mImage!!.render(0, 0, mBitmap, 0, 0, mImage!!.width, mImage!!.height, false, 0)
}
callback.invalidateImage(this)
} else {
// Remove from the set if the reference has been cleared or
// it can't be used.
iterator.remove()
}
}
return hasCallback
}
override fun run() {
// Check recycled
if (mBitmap.isRecycled || mImage == null) {
mRunning = false
return
}
mImage!!.advance()
if (notifyUpdate()) {
if (mRunning) {
HANDLER.postDelayed(this, Math.max(0, mImage!!.delay).toLong())
}
} else {
mRunning = false
}
}
interface Callback {
fun invalidateImage(who: ImageBitmap?)
}
get() = mImage!!.height
companion object {
private val HANDLER = Handler(Looper.getMainLooper())
/**
* Decode `InputStream`, then create image.
*/
@Throws(ImageDecoder.DecodeException::class)
@JvmStatic
fun decode(fd: FileDescriptor): ImageBitmap? {
val image = Image.decode(fd, false)
return if (image != null) {
create(image)
} else {
null
}
}
/**
* Create `ImageBitmap` from `Image`.
* It is not recommended. Use [.decode] if you can.
*
* @param image the image should not be used before and
* it must not be recycled. And the image should
* not be used directly anymore.
*/
fun create(image: Image): ImageBitmap? {
return if (!image.isRecycled) {
try {
ImageBitmap(image)
} catch (e: OutOfMemoryError) {
image.recycle()
null
}
} else {
null
}
}
/**
* Create `ImageBitmap` from `Bitmap`.
*
* @param bitmap the bitmap should not be recycled
*/
fun create(bitmap: Bitmap): ImageBitmap? {
return if (!bitmap.isRecycled) {
ImageBitmap(bitmap)
} else {
null
}
fun decode(stream: FileInputStream): ImageBitmap {
val image = Image.decode(stream)
return ImageBitmap(image)
}
}
}
@@ -1,87 +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.image
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
/**
* A drawable to draw [ImageBitmap]
*/
class ImageDrawable(imageBitmap: ImageBitmap) : Drawable(), Animatable, ImageBitmap.Callback {
private val mImageBitmap: ImageBitmap
private val mPaint: Paint
init {
if (!imageBitmap.obtain()) {
throw Exception()
}
mImageBitmap = imageBitmap
mPaint = Paint(Paint.FILTER_BITMAP_FLAG)
// Add callback
imageBitmap.addCallback(this)
}
fun recycle() {
mImageBitmap.removeCallback(this)
mImageBitmap.release()
}
override fun draw(canvas: Canvas) {
mImageBitmap.draw(canvas, null, bounds, mPaint)
}
override fun setAlpha(alpha: Int) {
mPaint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.colorFilter = colorFilter
}
override fun getOpacity(): Int {
return if (mImageBitmap.isOpaque) PixelFormat.OPAQUE else PixelFormat.UNKNOWN
}
override fun getIntrinsicWidth(): Int {
return mImageBitmap.width
}
override fun getIntrinsicHeight(): Int {
return mImageBitmap.height
}
override fun start() {
mImageBitmap.start()
}
override fun stop() {
mImageBitmap.stop()
}
override fun isRunning(): Boolean {
return mImageBitmap.isRunning
}
override fun invalidateImage(who: ImageBitmap?) {
invalidateSelf()
}
}
@@ -154,6 +154,7 @@ public abstract class StageActivity extends EhActivity {
mDelaySceneTagList.addAll(list);
}
mIdGenerator.lazySet(savedInstanceState.getInt(KEY_NEXT_ID));
updateBackPressCallBackStatus();
}
if (mStageId == IntIdGenerator.INVALID_ID) {
@@ -561,12 +562,8 @@ public abstract class StageActivity extends EhActivity {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss();
fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss();
} else {
fragmentManager.beginTransaction().detach(fragment).attach(fragment).commitAllowingStateLoss();
}
fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss();
fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss();
updateBackPressCallBackStatus();
}
@@ -589,6 +586,48 @@ public abstract class StageActivity extends EhActivity {
callback.setEnabled(shouldEnable);
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
int size = mSceneTagList.size();
String tag = mSceneTagList.get(size - 1);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "onProvideAssistContent: Can't find scene by tag: " + tag);
return;
}
if (fragment instanceof SceneFragment scene) {
scene.onProvideAssistContent(outContent);
} else {
Log.e(TAG, "onProvideAssistContent: The fragment is not SceneFragment");
}
}
public SceneFragment findSceneByTag(String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null) {
return (SceneFragment) fragment;
} else {
return null;
}
}
@Nullable
public Class<?> getTopSceneClass() {
int index = mSceneTagList.size() - 1;
if (index < 0) {
return null;
}
String tag = mSceneTagList.get(index);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (null == fragment) {
return null;
}
return fragment.getClass();
}
class BackPressCallBack extends OnBackPressedCallback {
public BackPressCallBack(boolean enabled) {
super(enabled);
@@ -614,50 +653,6 @@ public abstract class StageActivity extends EhActivity {
}
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
int size = mSceneTagList.size();
String tag = mSceneTagList.get(size - 1);
SceneFragment scene;
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "onProvideAssistContent: Can't find scene by tag: " + tag);
return;
}
if (!(fragment instanceof SceneFragment)) {
Log.e(TAG, "onProvideAssistContent: The fragment is not SceneFragment");
return;
}
scene = (SceneFragment) fragment;
scene.onProvideAssistContent(outContent);
}
public SceneFragment findSceneByTag(String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null) {
return (SceneFragment) fragment;
} else {
return null;
}
}
@Nullable
public Class<?> getTopSceneClass() {
int index = mSceneTagList.size() - 1;
if (index < 0) {
return null;
}
String tag = mSceneTagList.get(index);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (null == fragment) {
return null;
}
return fragment.getClass();
}
private final class SceneViewComparator implements Comparator<View> {
private int getIndex(View view) {
@@ -104,17 +104,13 @@ final class DocumentsContractApi19 {
return true;
}
// Writable normal files considered writable
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type)
&& (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) {
// Directories that allow create considered writable
return true;
} else if (!TextUtils.isEmpty(type)
&& (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) {
// Writable normal files considered writable
return true;
}
return false;
} else return !TextUtils.isEmpty(type)
&& (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0;
}
public static boolean delete(Context context, Uri self) {
@@ -25,7 +25,7 @@ import java.io.RandomAccessFile;
class RawRandomAccessFile implements UniRandomAccessFile {
private RandomAccessFile mFile;
private final RandomAccessFile mFile;
RawRandomAccessFile(RandomAccessFile file) {
mFile = file;
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff. Show more