Compare commits

..
897 Commits
Author SHA1 Message Date
FooIbar 36b16d7be8 Release 1.8.9.3-gms
Releases / Build (push) Has been cancelled
2023-08-17 23:00:54 +08:00
FooIbar fab5532407 Revert "Remove embedded Cronet library"
This reverts commit 9c5eddb9
2023-08-17 23:00:08 +08:00
FooIbar 8ad4e8640f Merge branch '1.8.x.x' into 1.8.9.x-gms
# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/com/hippo/ehviewer/util/CronetUtils.kt
2023-08-17 22:59:37 +08:00
FooIbar ca74bfe2e4 Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-17 22:52:55 +08:00
renovate[bot] 41767f0fd7 Update dependency gradle to v8.3 2023-08-17 14:50:21 +00:00
FooIbar c62ca4dd23 Fix DownloadsScene crash
java.lang.LinkageError: Method com.hippo.ehviewer.databinding.ActivityMainBinding com.hippo.ehviewer.ui.scene.DownloadsScene.getBinding() overrides final method in class Lcom/hippo/ehviewer/ui/scene/SearchBarScene
2023-08-17 20:24:34 +08:00
FooIbar 9c5eddb903 Remove embedded Cronet library 2023-08-17 17:51:44 +08:00
FooIbar 4a0df07805 Set User-Agent in CronetEngine.Builder 2023-08-17 00:03:13 +08:00
FooIbar 37a3edd44b Revert "Add quichint"
This reverts commit 4e427f27
2023-08-16 23:50:20 +08:00
FooIbar f117abd483 Revert "Add option for QUIC support"
This reverts commit 861c05db
2023-08-16 23:27:38 +08:00
FooIbar 4593714298 Cleanup DownloadManager 2023-08-16 23:12:18 +08:00
FooIbar a148fd6ec3 Use flow for DownloadsScene UI updates 2023-08-16 23:12:18 +08:00
FooIbar 23a44e2f3b Migrate DownloadAdapter to ListAdapter 2023-08-16 20:23:57 +08:00
FooIbar 01e03ec0f0 Support search in DownloadsScene 2023-08-15 23:03:52 +08:00
FooIbar 5b5a2a9a33 Release 1.8.9.2-gms
Releases / Build (push) Has been cancelled
2023-08-15 17:49:53 +08:00
FooIbar 7719312355 Merge branch '1.8.x.x' into 1.8.9.x-gms 2023-08-15 17:49:46 +08:00
FooIbar 4594ac592c Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-15 17:44:38 +08:00
FooIbar eee0f2dded Fix FavoritesScene crash 2023-08-15 17:24:21 +08:00
FooIbar b4080302c3 Refine GalleryListParser 2023-08-15 16:22:55 +08:00
renovate[bot] 337e210b5b Update dependency com.google.android.material:material to v1.11.0-alpha02 2023-08-15 05:08:26 +00:00
FooIbar 48f1b0a958 Release 1.8.9.1-gms
Releases / Build (push) Has been cancelled
2023-08-14 23:18:57 +08:00
FooIbar 9abeba6b15 Merge branch '1.8.x.x' into 1.8.9.x-gms 2023-08-14 23:18:34 +08:00
FooIbar a05b9174d4 Tweak EhScreen layout 2023-08-14 23:12:39 +08:00
FooIbar aa8adfb6f7 Use LazyColumn in showSelectItem() 2023-08-14 23:10:32 +08:00
FooIbar 1034fcd7b0 Fix favorite button color 2023-08-14 21:11:55 +08:00
FooIbar 69a26aeedb Implement swipe-to-remove in Downloads and Favorites 2023-08-14 21:11:55 +08:00
FooIbar dd607b41e9 Show search fab in FavoritesScene 2023-08-13 19:13:08 +08:00
FooIbar fd45319715 Fix toplist 2023-08-13 00:14:44 +08:00
FooIbar c34ba56896 Start a new fragment if search was performed from Popular or Toplist 2023-08-12 22:47:20 +08:00
FooIbar 6e00a4133c Fix nav checked item 2023-08-12 17:51:52 +08:00
FooIbar 75bec4314e Release 1.8.9.0-gms
Releases / Build (push) Has been cancelled
2023-08-11 16:03:44 +08:00
FooIbar b767c041b9 Load Cronet from Google Play services 2023-08-11 15:56:21 +08:00
FooIbar 4e916731b3 Release 1.8.9.0
Releases / Build (push) Has been cancelled
2023-08-11 14:29:52 +08:00
FooIbar 6c24407fb7 Update releases.yml 2023-08-11 14:29:52 +08:00
FooIbar 9924b6ec0b Cleanup layout 2023-08-11 13:49:16 +08:00
FooIbar 4d09504abc Fix swipe-to-refresh 2023-08-11 13:49:15 +08:00
FooIbar f901bd2336 Fix navigation transitions 2023-08-11 13:49:13 +08:00
FooIbar 187c35fe88 Fix "Copied to clipboard" message not showing in API 32 and lower 2023-08-11 13:48:51 +08:00
FooIbar cc94bed772 Fix error message when no hits found 2023-08-11 13:48:51 +08:00
FooIbar 8c9cdf46db Remove predictive back support
Reland after #1322 resolved
2023-08-11 13:48:51 +08:00
FooIbar 09dd200655 Migrate right drawer to side sheet 2023-08-11 13:38:17 +08:00
FooIbar 38294e8cfc Fix deprecated function 2023-08-11 13:38:17 +08:00
FooIbar 87a42cf53d Revert obsolete Workaround
This reverts commit 576ff1b0

Reason for revert: Fixed in `androidx.core:core:1.12.0-beta01`.
https://android.googlesource.com/platform/frameworks/support/+/8f7e02550620919fafb4c7fef8743f8c20562945
2023-08-11 13:38:15 +08:00
renovate[bot] 2adf2c15fa Update dependency androidx.browser:browser to v1.6.0 2023-08-10 15:31:44 +00:00
renovate[bot] 99ac2a287e Update room_version to v2.6.0-alpha03 2023-08-10 13:08:56 +00:00
renovate[bot] 371ccab596 Update nav_version to v2.7.0 2023-08-10 11:39:12 +00:00
renovate[bot] 590c505f33 Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha02 2023-08-10 06:27:27 +00:00
renovate[bot] 01c2f3ea69 Update dependency androidx.core:core-ktx to v1.12.0-rc01 2023-08-10 04:29:43 +00:00
renovate[bot] 4c4eec118d Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha12 2023-08-10 01:27:55 +00:00
renovate[bot] 310be050d8 Update dependency androidx.collection:collection-ktx to v1.3.0-beta01 2023-08-09 22:24:00 +00:00
FooIbar 181e0301aa Fix onBackPressed callbacks order 2023-08-07 08:48:21 +08:00
FooIbar a86ca2e82c Move favorite button below header 2023-08-06 22:38:34 +08:00
FooIbar 85c911cf35 Tweak GalleryDetailScene button layout 2023-08-06 22:38:34 +08:00
FooIbar db57e2dde6 Use mobile UA for Cloudflare bypass
Allow custom UA in the future?
2023-08-05 18:25:39 +08:00
FooIbar c163ad42a6 Remove experimental status of Cloudflare bypass 2023-08-05 17:14:06 +08:00
FooIbar a2af3a4652 Manually resolve Cloudflare challenge if failed to bypass 2023-08-05 17:14:06 +08:00
FooIbar 59a9448d70 Restore default favorite slot 2023-08-04 17:37:26 +08:00
FooIbar 22b87de29d Don't show cloud favorites when not logged in 2023-08-04 17:37:26 +08:00
FooIbar d98292da1d Refine Cloudflare challenges detection 2023-08-04 05:46:51 +08:00
renovate[bot] 5422c04c53 Update plugin com.android.application to v8.1.0 2023-08-03 21:39:26 +08:00
FooIbar 0a5783de89 Fix compatibility with AGP 8.1.0 2023-08-03 21:39:14 +08:00
renovate[bot] 717e2191b0 Update plugin com.google.devtools.ksp to v1.9.0-1.0.13 2023-08-03 10:20:10 +08:00
FooIbar eedc3ba5eb Drop CheckableComposeView
It was required by EasyRecyclerView
2023-08-03 10:18:22 +08:00
renovate[bot] 30ee1816df Update dependency com.squareup.okio:okio-jvm to v3.5.0 2023-08-02 18:03:57 +00:00
FooIbar 940047de11 Refine Cloudflare challenge detection 2023-08-02 17:21:40 +08:00
tarsin c0e2041efe Cleanup code 2023-08-02 10:25:09 +08:00
tarsin 8f9d2adcfe Fixup to use compose FontFamily 2023-08-02 10:19:39 +08:00
tarsin 1bfda83665 Introduce DialogState.awaitInputText 2023-08-02 10:14:41 +08:00
tarsin 39b896e1b9 Cleanup dialog api 2023-08-02 09:18:27 +08:00
tarsin cfe7ee7f9c Convert to use tachiyomi fastscroller impl 2023-08-02 08:42:55 +08:00
FooIbar 2a95d6707a Implement Context.createReaderThemeContext() 2023-08-02 07:07:30 +08:00
renovate[bot] f4fe5f1783 Update ktor to v2.3.3 2023-08-01 18:57:09 +00:00
FooIbar 10040b2923 Minor tweaks 2023-08-01 21:04:22 +08:00
tarsin 42c56616fb Apply compose compiler report plugin 2023-08-01 09:55:35 +08:00
tarsin f8c926e1cd Cleanup unused res 2023-08-01 09:38:08 +08:00
tarsin 2adf620059 Let suggestion content padding a navbar 2023-08-01 09:30:31 +08:00
tarsin 4bdc182520 Revert "Optimize suggestion data fetch"
Revert reason: Fix OOB
This reverts commit bc4c7f62a0.
2023-08-01 09:23:40 +08:00
tarsin bc4c7f62a0 Optimize suggestion data fetch 2023-08-01 09:17:41 +08:00
tarsin b2d0c59913 Fixup suggestion color 2023-08-01 09:03:58 +08:00
tarsin ceb5424772 Cleanup SuggestionProvider 2023-08-01 08:54:46 +08:00
tarsin 5a5a5da2d9 Convert SearchBarScene SuggestRV to LazyColumn 2023-08-01 08:42:15 +08:00
tarsin 0ff9b03596 Convert TaskPreferenceDialog to compose 2023-08-01 07:37:30 +08:00
FooIbar acbe4eafa9 Support clearing search history 2023-08-01 06:36:00 +08:00
FooIbar 0a5600d949 Cleanup code 2023-08-01 06:36:00 +08:00
tarsin 63a83257fe Revert category text color to onSurface 2023-07-31 13:07:37 +08:00
tarsin d115e28403 Fix DownloadsScene category 2023-07-31 12:52:08 +08:00
tarsin 3d1ae0acfc Introduce harmonizeWithRole to keep target role chroma and tone 2023-07-31 12:35:38 +08:00
tarsin a3a01065c0 Harmonize category color 2023-07-31 11:13:16 +08:00
tarsin ae33d692bf Clip category a small corner 2023-07-31 10:46:54 +08:00
tarsin 39f55d7db1 Upgrade r8 2023-07-29 20:27:03 +08:00
tarsin a86f8d6096 Upgrade accompanist to v0.31.6rc (#1301)
* Upgrade accompanist to v0.31.6rc

* Update build.gradle.kts
2023-07-29 19:14:39 +08:00
tarsin b28fc3ffc4 Make cronetRequest.execute(..., () -> R): R 2023-07-28 18:28:07 +08:00
tarsin 5d4f544b94 No more content length validation 2023-07-28 18:18:01 +08:00
tarsin b0f0686821 Convert HomeParser to use a bytebuffer 2023-07-28 17:49:41 +08:00
tarsin b7c8afe7d2 Initial fetchCompat okhttp layer 2023-07-28 17:42:17 +08:00
tarsin 02855b68e6 Initial let parser use bytebuffer 2023-07-28 17:24:42 +08:00
tarsin cf8097babc Don't truncate fc, receivedByteCount is unreliable 2023-07-28 16:54:46 +08:00
tarsin 2d5a2022b4 Convert all GalleryListParser to rust 2023-07-28 15:30:57 +08:00
tarsin 2fac22970e Fixup! Cleanup TorrentParser 2023-07-28 12:31:44 +08:00
tarsin 8c10836e6e Cleanup TorrentParser 2023-07-28 12:30:43 +08:00
tarsin 69bba560f0 Convert to use regex-lite 2023-07-28 11:56:16 +08:00
tarsin a9d5c15bd9 Fixup codestyle 2023-07-28 11:37:59 +08:00
tarsin 1d0812ddaf Fixup codestyle 2023-07-28 11:15:48 +08:00
tarsin 02bdc9c1a3 Refine rust glist parser, do not allocate subhtml 2023-07-28 10:41:44 +08:00
tarsin 6a08460e8e Fix thumbnail mode parser 2023-07-28 10:02:04 +08:00
tarsin 091ad5cf0f Cleanup GalleryListParser 2023-07-28 09:25:56 +08:00
tarsin f911e4629c Drop jnix net extension 2023-07-28 07:18:28 +08:00
tarsin 0265ddc6e1 Introduce CronetRequest.copyToChannel 2023-07-27 12:02:30 +08:00
tarsin ead2300d90 Refactor package layout 2023-07-27 11:56:22 +08:00
tarsin b3a8547527 Drop okhttptransport since most file download job use cronet directly now 2023-07-27 11:52:31 +08:00
tarsin 37a5dbd20d Fix animated CoilImageGetter 2023-07-27 11:31:54 +08:00
tarsin c20dc66db3 Fix GalleryDetailScene comment ripple 2023-07-27 10:43:46 +08:00
tarsin 31c32123c9 Drop URLImageGetter 2023-07-27 10:25:08 +08:00
tarsin 14faa38de3 Fix SegmentedButton shape 2023-07-27 07:59:02 +08:00
tarsin 4827b7e077 Convert to use SingleChoiceSegmentedButtonRow 2023-07-27 07:53:55 +08:00
renovate[bot] 2d9a17bd25 Update dependency androidx.core:core-ktx to v1.12.0-beta01 2023-07-27 07:38:36 +08:00
renovate[bot] ce3ad45c09 Update dependency dev.chrisbanes.compose:compose-bom to v2023.07.00-alpha02 2023-07-27 07:38:27 +08:00
renovate[bot] 44fcc21d9c Update paging to v3.2.0 2023-07-27 07:38:17 +08:00
renovate[bot] 5c1a6808d7 Update dependency androidx.recyclerview:recyclerview to v1.3.1 2023-07-27 07:24:58 +08:00
renovate[bot] d461b1b8b6 Update dependency composeOptions to v1.5.1 2023-07-27 07:24:44 +08:00
renovate[bot] d74b0448d5 Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha11 2023-07-26 21:24:28 +00:00
renovate[bot] ed2a37c615 Update dependency androidx.navigation:navigation-compose to v2.7.0-rc01 2023-07-26 19:49:59 +00:00
tarsin 34c51d3115 Cleanup CronetHttpUriFetcher 2023-07-26 17:25:41 +08:00
tarsin f85d36d14d Cleanup CronetHttpUriFetcher 2023-07-26 17:17:31 +08:00
tarsin 9818f227d7 Initial Jvmheapless IO for Coil 2023-07-26 17:01:15 +08:00
tarsin 01353e05cc Cleanup CronetRequest 2023-07-26 16:16:32 +08:00
tarsin 02eaedd6a0 Lazy borrow cronet bytebuffer 2023-07-26 15:52:55 +08:00
renovate[bot] c71c872aa2 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.3 2023-07-25 21:52:29 +00:00
renovate[bot] d79dc0c630 Update Rust crate quick-xml to 0.30 2023-07-24 07:30:53 +08:00
FooIbar 46cf695bde Fix gallery detail error tip not showing 2023-07-22 21:45:44 +08:00
FooIbar c913a7eb9e Fix NPE in H@H archive download 2023-07-22 18:07:22 +08:00
tarsin 887c46c87f Hide LA symbols 2023-07-22 17:53:00 +08:00
tarsin e4d3501f93 Use fat lto 2023-07-22 17:18:45 +08:00
FooIbar add98bb630 Unescape gallery info fetched from API 2023-07-22 16:36:59 +08:00
tarsin 05651584dc Fixup install ndk 2023-07-22 10:56:00 +08:00
tarsin 37171840d3 Fixup install ndk 2023-07-22 10:55:06 +08:00
tarsin 4ee8fbedbf Fixup install ndk 2023-07-22 10:52:48 +08:00
tarsin 274e6a0666 Switch to ndk r26b1 2023-07-22 10:48:01 +08:00
tarsin b26686823e Do not build libarchive test 2023-07-22 10:27:50 +08:00
tarsin 64354ea511 Xz 5.4.3 2023-07-22 10:16:27 +08:00
tarsin 07cce5bbad Libarchive 3.7.0 2023-07-22 09:59:33 +08:00
FooIbar bbab9522df Fix category badge not showing 2023-07-22 06:28:51 +08:00
FooIbar 1a6d839ca0 Partly revert 0d666a6 to reduce network requests 2023-07-22 06:27:09 +08:00
FooIbar 0d666a633f Add dirname field for DownloadInfo 2023-07-21 20:36:19 +08:00
tarsin b24679adbd Update r8 2023-07-21 09:12:55 +08:00
tarsin 90b4d643d4 Cleanup dependencies 2023-07-21 08:59:18 +08:00
tarsin 469f2871f7 Revert to use quick-xml for url unescape 2023-07-21 08:54:06 +08:00
tarsin c7766de766 Strip unused features 2023-07-21 08:22:28 +08:00
tarsin 314de6a6a7 Set opt-level to 's', we need vectorize html parser 2023-07-21 07:48:01 +08:00
tarsin 9c148dd58d Set rust codegen units to 1 for release profile 2023-07-21 07:34:31 +08:00
tarsin 1ae469facc Drop obsolete workaround (#1274)
* Drop obsolete workaround

* Update build.gradle.kts
2023-07-21 07:05:51 +08:00
renovate[bot] eada6641e4 Update plugin com.google.devtools.ksp to v1.9.0-1.0.12 2023-07-21 06:19:24 +08:00
FooIbar 2eb50803b5 Fix downloads import order 2023-07-21 06:19:10 +08:00
FooIbar 29e3e876c7 Upgrade db, add foreign key for download label 2023-07-21 06:19:10 +08:00
FooIbar 1c38dbaa0b Upgrade db, add unique index for download label 2023-07-21 06:19:10 +08:00
FooIbar 31dcd349df Fix delete download not working 2023-07-20 19:00:57 +08:00
FooIbar ee504e3361 Upgrade db, extract common gallery info 2023-07-20 19:00:57 +08:00
FooIbar 11426e1c4c Upgrade db, add galleries table 2023-07-20 19:00:57 +08:00
tarsin 13346f1124 Drop explicitly copy constructor 2023-07-19 17:28:27 +08:00
FooIbar f38f3989e7 Tweak db migration, cleanup code
Keep the time field in DownloadInfo for future sort by start / complete time function
2023-07-19 07:17:21 +08:00
FooIbar 5c33ca1c9a [DO NOT USE] Refine drag and drop reordering, part 3/3 2023-07-19 07:17:21 +08:00
FooIbar 3599ffb805 [DO NOT USE] Refine drag and drop reordering, part 2/3 2023-07-19 07:17:21 +08:00
FooIbar 344cd1305c [DO NOT USE] Refine drag and drop reordering, part 1/3 2023-07-19 07:17:21 +08:00
renovate[bot] 84fe0b9cb7 Update dependency composeOptions to v1.5.0 2023-07-18 21:56:53 +00:00
tarsin e5963208b1 Fixup codestyle 2023-07-18 18:28:27 +08:00
tarsin 8bc84b404d Reimplement groupByToObserved 2023-07-18 18:23:40 +08:00
tarsin 80d75661c1 Fixup! Refactor EhFilter to fix concurrency modification 2023-07-18 18:14:58 +08:00
tarsin 1c74a422bc Refactor EhFilter to fix concurrency modification 2023-07-18 17:34:51 +08:00
FooIbar f06c2d6805 Adjust default aspect in grid mode 2023-07-18 14:22:41 +08:00
FooIbar fdbe06e8c5 Use handle instead of long press for drag 2023-07-18 07:05:13 +08:00
FooIbar 15a9c4ee45 Move to recyclerview-selection, part 2/2 2023-07-18 07:05:13 +08:00
FooIbar 38c90f35f2 Move to recyclerview-selection, part 1/2 2023-07-18 07:05:13 +08:00
renovate[bot] bf3ee4cce2 Update plugin com.mikepenz.aboutlibraries.plugin to v10.8.3 2023-07-17 15:52:59 +00:00
renovate[bot] 9553491040 Update dependency com.mikepenz:aboutlibraries-core to v10.8.3 2023-07-17 13:29:54 +00:00
tarsin 0253d92c7f Cleanup code 2023-07-16 22:28:07 +08:00
tarsin e2ac340b6f Refine SelectableGalleryInfoRecyclerView 2023-07-16 22:22:29 +08:00
tarsin 44b60185cc Refine SelectableGalleryInfoRecyclerView 2023-07-16 22:12:15 +08:00
tarsin 9b3ffd1f2c Refactor EasyRecyclerView 2023-07-16 21:59:48 +08:00
tarsin 935c8e7d15 Remove easyrv usage in GalleryCommentsScene 2023-07-16 21:15:58 +08:00
tarsin 8f5094efef Cleanup EasyRv 2023-07-16 18:45:12 +08:00
tarsin 91accb6655 Cleanup all unnecessary easyrv usage, prepare to drop easyrv 2023-07-16 18:38:54 +08:00
FooIbar 4f23cf5d8d Fix local favorite count not updating 2023-07-16 16:33:29 +08:00
FooIbar 39f76246c8 Fix favorites move from local to cloud 2023-07-16 16:33:29 +08:00
FooIbar 8ac2b027ec Remove deprecated hosts 2023-07-16 16:33:29 +08:00
FooIbar 919d22ebf1 Gradle configuration cache compatibility 2023-07-16 16:33:29 +08:00
FooIbar 14a28c681a Do db migration, drop history favorite slot trick 2023-07-15 17:39:43 +08:00
FooIbar 388bccc468 Make QuickSearch name non-null 2023-07-15 17:39:43 +08:00
FooIbar 639d94795c Check initialization of mSpiderQueen 2023-07-15 14:20:38 +08:00
FooIbar 51e2f44868 Fix swipe refresh and error tip click refresh 2023-07-15 06:44:59 +08:00
FooIbar 92b9b9879c Fix sad panda check on login 2023-07-14 18:18:58 +08:00
FooIbar c867a74aea Tweak original image download 2023-07-14 18:18:45 +08:00
FooIbar e9a1bf22b1 Tweak archive download and image limit reset 2023-07-14 18:18:45 +08:00
FooIbar 46271fb0d2 Subscribe reading mode and orientation type changes 2023-07-13 07:00:18 +08:00
renovate[bot] 7438b17366 Update dependency io.arrow-kt:arrow-stack to v1.2.0 2023-07-12 18:38:41 +00:00
FooIbar 8306640ed6 Don't intercept on Varnish errors 2023-07-12 18:33:01 +08:00
tarsin 39b7623688 Flatten layout 2023-07-12 17:21:22 +08:00
tarsin 41b46035e4 Fix DownloadsScene leak 2023-07-12 16:22:20 +08:00
tarsin 10045494b8 Fix GalleryCommentsScene binding leak 2023-07-12 16:11:28 +08:00
tarsin ea67a022d8 Fix leaking 2023-07-12 15:56:20 +08:00
tarsin 203c89e3a8 Fixup! Fix a itemTouchHelper rv leak 2023-07-12 15:51:07 +08:00
tarsin 1c88be6f82 Fix a itemTouchHelper rv leak 2023-07-12 15:47:40 +08:00
tarsin 2a19bcd73b Fix a rv adapter leak 2023-07-12 15:34:07 +08:00
tarsin 9f29594ad6 Fix paging3 dependency 2023-07-12 15:22:27 +08:00
tarsin 9677e6ac80 Add leakcanary debug impl 2023-07-12 15:20:43 +08:00
tarsin 1590ca4b80 Convert DownloadService to use a deferred DownloadManager 2023-07-12 15:04:40 +08:00
tarsin effe7530ed Fixup! suspend db query 2023-07-12 12:24:31 +08:00
tarsin 19f270e320 Fixup! suspend db query 2023-07-12 12:22:55 +08:00
tarsin 25d876f8b9 Initial suspend all db query
TODO: Cleanup DownloadManager
2023-07-12 12:21:05 +08:00
tarsin 179e470154 Make DownloadScreen scrollable 2023-07-12 11:08:02 +08:00
tarsin 5cf122c30c Suspend more db 2023-07-12 10:41:56 +08:00
tarsin 36895de83b Introduce runAssertingNotMainThread as workaround 2023-07-12 10:37:00 +08:00
tarsin fed5285a31 Suspend more db query 2023-07-12 10:30:46 +08:00
tarsin b1fbefc09e Cleanup DownloadManager 2023-07-12 10:20:41 +08:00
tarsin 2fd9982c37 Cleanup EHDB nullability 2023-07-12 10:07:26 +08:00
tarsin 89a108574a Suspend DownloadLabelDao delete 2023-07-12 10:05:24 +08:00
tarsin 28ce6ee13e Suspend DownloadDirnameDao 2023-07-12 10:01:26 +08:00
tarsin 4441ea2c5d Suspend DownloadDirnameDao load 2023-07-12 09:58:31 +08:00
tarsin 02489893ec Suspend DownloadDirnameDao update insert 2023-07-12 09:46:09 +08:00
tarsin 62c3e9290e Suspend DownloadDirnameDao deleteByKey 2023-07-12 09:35:32 +08:00
tarsin 3f53710c5b Restore allowMainThreadQueries, prepare to suspendize DownloadManager 2023-07-12 09:31:16 +08:00
tarsin 389f27dfa1 Cleanup unused dao method 2023-07-12 09:30:13 +08:00
tarsin e2d03eb928 Purify SearchDao 2023-07-12 09:26:20 +08:00
tarsin 37ec49ed69 Suspend parser 2023-07-12 09:22:24 +08:00
tarsin 4694245b51 Suspend more db queries, part of #629 2023-07-12 09:15:01 +08:00
tarsin 60b77145a9 Prepare to drop allowMainThreadQueries 2023-07-12 08:37:46 +08:00
tarsin 28a6f8cd2b Refine package layout 2023-07-12 08:34:55 +08:00
tarsin 02a3c1c54b Cleanup kt style builder 2023-07-12 08:32:43 +08:00
tarsin 09df7cc39b Cleanup kt style builder 2023-07-12 08:27:33 +08:00
tarsin a4ce60c849 Refactor package layout 2023-07-12 08:03:45 +08:00
tarsin d74f18012b Refactor package layout 2023-07-12 07:55:54 +08:00
tarsin 3609379284 Refactor package layout 2023-07-12 07:48:21 +08:00
tarsin 19f9105640 Fixup AboutScreen edgeToEdge 2023-07-11 19:43:05 +08:00
tarsin 24664d4e83 Merge EhPreviewItem and its placeholder 2023-07-11 17:42:32 +08:00
tarsin e16b5b0437 Cleanup layout 2023-07-11 16:29:02 +08:00
FooIbar 94747da3d0 Tweak the layout of the favorite dialog 2023-07-11 14:43:37 +08:00
tarsin df8c70d8e6 Move GalleryListScene to paging3 (#1245)
* Initial WIP move GalleryListScene to paging3

* Refine

* Refine

* Refine

* Refine

* Refine

* Refine

* Typesafe State

* Refine

* Refine

* Refine

* Refine

* Cleanup code

* Cleanup code
2023-07-11 10:40:03 +08:00
tarsin 7fd3361608 Refine FavoritesScene 2023-07-11 07:35:13 +08:00
FooIbar d9522e4e89 Implement automatic update checks 2023-07-11 06:53:09 +08:00
FooIbar a8c96fa7df Introduce AppUpdater 2023-07-11 06:53:09 +08:00
renovate[bot] 5570933be4 Update dependency gradle to v8.2.1 2023-07-10 15:27:02 +00:00
FooIbar fb6cb8d3fb Generate repo name at build time 2023-07-10 22:13:55 +08:00
FooIbar 4ca7b6b11c Show changelog for CI build 2023-07-10 22:13:55 +08:00
renovate[bot] 5c377d346e Update dependency com.mikepenz:aboutlibraries-core to v10.8.2 2023-07-10 19:34:30 +08:00
tarsin 1dd0eeb2ed Rename rememberMemorized to rememberInVM 2023-07-10 19:28:42 +08:00
tarsin 3f6abd6380 Cleanup rememberMemorized 2023-07-10 19:27:44 +08:00
tarsin 407c55dbdc Properly store GalleryPreviewScreen data 2023-07-10 18:36:38 +08:00
tarsin 74afa6c764 Cleanup FavoritesScene 2023-07-10 18:27:31 +08:00
tarsin 60ccd988cb Introduce lazyMut 2023-07-10 18:26:15 +08:00
tarsin 505ba6e4e3 Move some persistent data to VM as in-memory storage 2023-07-10 18:02:40 +08:00
tarsin d4d6fe56a9 Move FavouriteScene to paging3 (#1238)
* Initial move favScene to paging3

* Refine

* Refine

* Refine

* Refine

* Implement goto bottom

* Refine

* Implement Jumpto

* Refine

* Refine

* Refine

* Refine

* Refine

* Cleanup code

* Cleanup code

* Refine

* Refine

* Do load in IOContext
2023-07-10 14:00:45 +08:00
tarsin 740fff2178 Add paging3 runtime dependency 2023-07-10 08:34:47 +08:00
tarsin 54a2ea64bc Cleanup GalleryAdapter, prepare migrate to paging3 2023-07-10 08:30:38 +08:00
FooIbar 200882e13c Log details of bad response code error 2023-07-10 06:43:44 +08:00
FooIbar dac28b9b6a Refactor e-hentai API response parsing 2023-07-10 06:43:44 +08:00
renovate[bot] 7a8ae183da Update plugin com.mikepenz.aboutlibraries.plugin to v10.8.2 2023-07-09 18:32:08 +00:00
tarsin c163e24434 Refine ComposeFavScene 2023-07-09 17:14:04 +08:00
tarsin eed30084bc Refine ComposeFavScene 2023-07-09 16:55:39 +08:00
tarsin 137a51dad0 Refine ComposeFavScene SearchBar 2023-07-09 16:29:31 +08:00
tarsin b71d326620 Refine ComposeFavScene 2023-07-09 16:21:57 +08:00
tarsin 371622770b Refine ComposeFavScene 2023-07-09 16:18:23 +08:00
tarsin 2b6bcf6698 Refactor ComposeFavScene 2023-07-09 16:05:45 +08:00
tarsin bc33ea84f2 Initial load Fav source 2023-07-09 16:00:57 +08:00
tarsin 3d7210ec08 Prepare to rewrite FavScene with Compose 2023-07-09 15:40:37 +08:00
tarsin e7bed5f5eb Remove redundant TODO 2023-07-09 15:29:03 +08:00
tarsin a3237a024e Cleanup ComposeFragment 2023-07-09 15:28:29 +08:00
tarsin 662fc6859b Fix GalleryPreviewScreen page load 2023-07-09 13:27:20 +08:00
tarsin 5bb61959bd Add jumping support 2023-07-09 12:52:57 +08:00
tarsin 8ae75101ef Add LazyGridScrollbar for preview jump 2023-07-09 11:54:13 +08:00
tarsin 95fcab0980 Inline LazyColumnScrollbar 2023-07-09 11:36:37 +08:00
tarsin d2b141e218 Fix jump crash 2023-07-09 11:21:10 +08:00
tarsin 73eb91f4b4 Cleanup GalleryPreviewScreen 2023-07-09 10:49:43 +08:00
tarsin 61c51f8882 Fix MergeInterceptor 2023-07-09 10:49:00 +08:00
tarsin cb9f36dd52 Rewrite GalleryPreviewScreen Pager, Resolve #1152 2023-07-09 10:14:13 +08:00
tarsin 80b0203af2 Fix ci failed 2023-07-09 08:16:48 +08:00
tarsin 66155eed1c Fix crash when r8 shrink aboutlibraries json 2023-07-09 07:36:39 +08:00
renovate[bot] 144e8a2951 Update dependency com.mikepenz:aboutlibraries-core to v10.8.1 2023-07-09 06:48:37 +08:00
renovate[bot] 7c1f521960 Update plugin com.mikepenz.aboutlibraries.plugin to v10.8.1 2023-07-08 22:01:29 +00:00
tarsin dbe528269d Fix FavoritesScene crash again 2023-07-08 20:25:33 +08:00
tarsin 9b843e84b2 Fix FavoritesScene crash 2023-07-08 19:49:22 +08:00
tarsin c9ef0e5406 Fix read download button text maxline 2023-07-08 18:49:23 +08:00
tarsin 1a49b3d00b Cleanup unused field 2023-07-08 18:41:08 +08:00
tarsin 33502c88db Fix FavDrawer ListItem style 2023-07-08 18:32:41 +08:00
tarsin 6554735c63 Make Favscene drawer aware of Settings fav changes 2023-07-08 18:07:53 +08:00
tarsin 2360e6376e Convert FavSceneDrawer to Compose 2023-07-08 17:22:20 +08:00
tarsin b1e186a5f1 Bump MCA 2023-07-08 16:24:17 +08:00
tarsin ffd2b5a342 Fixup GalleryInfoBottomSheet impl 2023-07-08 16:21:28 +08:00
tarsin d5a16f50c5 Revert GalleryInfoBottomSheet to BottomSheetDialogFragment impl for predictive back 2023-07-08 16:12:03 +08:00
tarsin 5b03e67108 Drop unused code 2023-07-08 15:39:52 +08:00
tarsin eac5b2641b Fix galleryProvider init crash 2023-07-08 15:36:03 +08:00
tarsin e46008df55 Cleanup: Move ReaderProgressIndicator to compose 2023-07-08 14:36:42 +08:00
tarsin 07b1f39796 Move ReaderProgressIndicator to compose 2023-07-08 14:34:12 +08:00
tarsin 64d3849b62 Fixup PageSlider haptic feedback 2023-07-08 14:22:53 +08:00
tarsin 9d766f47c5 Fixup PageSlider theme 2023-07-08 14:20:22 +08:00
tarsin 64841e6e3b Move PageSlider to Compose 2023-07-08 14:11:47 +08:00
tarsin 571fbabf4f Restore PageIndicator padding 2023-07-08 13:54:38 +08:00
tarsin 12c3ad6d5f Fixup tachiyomi theme 2023-07-08 13:53:21 +08:00
tarsin 7c987ec868 Move PageIndicatorText to Compose 2023-07-08 13:47:39 +08:00
tarsin 27686e952a Resolve #1227 2023-07-08 06:58:24 +08:00
renovate[bot] de06c24924 Update dependency com.squareup.okio:okio-jvm to v3.4.0 2023-07-07 20:11:02 +00:00
tarsin 4e1b4ddf28 Kotlin 1.9.0 2023-07-07 07:12:07 +08:00
FooIbar ca21b5e713 Introduce <T> Request.executeAndParseAs() 2023-07-06 06:22:46 +08:00
FooIbar 5a6a01f813 Use kotlinx.serialization for json handling 2023-07-06 06:22:46 +08:00
FooIbar 16c6353459 Show changelog for release 2023-07-05 19:31:28 +08:00
FooIbar d1623d411a Show confirm dialog before downloading updates 2023-07-05 19:31:28 +08:00
tarsin 4e4b072c16 No more UIContext needed to modifyFav 2023-07-05 11:01:35 +08:00
tarsin f2601fb672 Refine check updates 2023-07-05 10:53:36 +08:00
tarsin 125c467c1b Fixup! Support install CI apk 2023-07-05 10:48:44 +08:00
tarsin 0b95c340d6 Support install CI apk 2023-07-05 10:47:36 +08:00
tarsin 79e0b33aad Extract the apk 2023-07-05 10:19:22 +08:00
tarsin f1f7b8dc88 Add no-op token to download CI 2023-07-05 10:04:47 +08:00
tarsin e97e539f92 Get CI archive url 2023-07-05 09:22:42 +08:00
tarsin 4b562f98f0 Get workflow url 2023-07-05 09:07:04 +08:00
tarsin bd06d76656 Cleanup code 2023-07-05 08:43:02 +08:00
tarsin 38ddca2342 Initial get latest version using github restful api 2023-07-05 08:39:22 +08:00
tarsin 1119798ce8 Initial get latest commitSha using github restful api 2023-07-05 08:32:21 +08:00
tarsin 1eeab2f03d Introduce preference to use CI update channel 2023-07-05 07:51:29 +08:00
FooIbar bfbe6b8e6a Translate tags when saving to quick search 2023-07-05 07:20:50 +08:00
FooIbar 4905c955bc Max characters limit is for ASCII characters 2023-07-05 07:20:36 +08:00
FooIbar 4cf698a10a Cleanup unused res 2023-07-04 21:20:24 +08:00
FooIbar 4b66f58fd3 Support adding favorite notes 2023-07-04 21:20:24 +08:00
FooIbar 325909401c Use named constant instead of magic number 2023-07-04 21:20:24 +08:00
FooIbar 652d6a2f28 Support displaying favorite notes 2023-07-04 21:20:24 +08:00
tarsin 7672a82260 Convert non-close callbackFlow to MutableSharedFlow 2023-07-04 11:22:01 +08:00
tarsin 0f37d17bf3 Cleanup HistoryScene 2023-07-04 10:54:56 +08:00
tarsin c923d74fec Drop explicated recompose 2023-07-04 10:49:30 +08:00
tarsin 2ba8640996 Let FavouriteStatusRouter handle historyDB as well 2023-07-04 10:39:03 +08:00
tarsin 0b1a0cade8 Cleanup FavouriteStatusRouter implementation 2023-07-04 10:24:08 +08:00
tarsin 78aab4f6c7 Cleanup FavouriteStatusRouter stateFlow 2023-07-04 10:18:14 +08:00
tarsin 77502e1a7c No need to query local db fav info 2023-07-04 10:14:30 +08:00
tarsin b90f221ede Drop isFavorited field 2023-07-04 10:11:54 +08:00
tarsin ee68b421d6 Cleanup FavouriteStatusRouter 2023-07-04 10:06:10 +08:00
tarsin 88be232e70 Drop a notifyDataSetChanged 2023-07-04 09:08:29 +08:00
FooIbar da9de7a823 Truncate file to the size actually written 2023-07-04 07:08:18 +08:00
FooIbar f08e124df6 Support downloading original images in reader 2023-07-03 22:55:05 +08:00
FooIbar ce888a0982 Unwrap IOException in getReadableString() 2023-07-03 22:55:05 +08:00
tarsin f59789a11e Fixup! Only emit info being add 2023-07-03 21:04:29 +08:00
tarsin c8a3db8a3e Fixup! Workaround ComposeView in RVView 2023-07-03 20:45:01 +08:00
tarsin 23366c8ea1 Workaround ComposeView in RVView 2023-07-03 20:35:04 +08:00
tarsin 6e65afa7e2 Share DownloadManager callback flow 2023-07-03 20:24:22 +08:00
tarsin 29b81aa90e Fix GalleryInfoListItem to use produceState 2023-07-03 20:13:25 +08:00
tarsin d85090787f Fix GalleryDetailScene DownloadInfoListener callbackFlow 2023-07-03 20:09:42 +08:00
tarsin 0eb3cc9073 Cleanup DownloadInfoListener callbackFlow 2023-07-03 20:03:48 +08:00
tarsin dfd9af8ee3 Convert DownloadInfoListener to callbackFlow 2023-07-03 19:57:28 +08:00
tarsin 1120c23709 Do db migration, make DownloadLabel nonnull 2023-07-03 18:46:43 +08:00
tarsin 975421044c Cleanup toMutable* usage 2023-07-03 18:37:50 +08:00
tarsin 802b63ed5a Cleanup DownloadManager nullability 2023-07-03 18:33:13 +08:00
tarsin d32a0d6b7f Cleanup DownloadManager 2023-07-03 18:32:20 +08:00
tarsin 0c5a1a6533 Cleanup DownloadManager 2023-07-03 18:30:35 +08:00
tarsin 61796f4cd3 Cleanup DownloadManager init code 2023-07-03 18:22:06 +08:00
tarsin 80726a7ff0 Cleanup DownloadManager 2023-07-03 18:04:34 +08:00
tarsin a2fdcc57f8 Cleanup DownloadManager 2023-07-03 17:57:05 +08:00
tarsin bffb32eafd Reduce allocation 2023-07-03 17:40:44 +08:00
tarsin 2c452d236c Cleanup mAllInfoMap init code 2023-07-03 17:37:58 +08:00
tarsin e7e036f7c6 Convert mAllInfoMap to LinkedHashMap and do query in composer thread 2023-07-03 17:25:28 +08:00
tarsin fbb702cacd Fix Deferred implementation, Resolve #1213 2023-07-03 17:13:12 +08:00
FooIbar 598438a5f9 Avoid rust jni log spam 2023-07-03 16:34:05 +08:00
FooIbar 24a1c94f04 Wrap exception as IOException in CloudflareInterceptor 2023-07-03 15:51:51 +08:00
tarsin e4e053a49b Clip CircularLayout item ripple 2023-07-03 10:29:43 +08:00
tarsin 8c0e217117 Refactor showSelectItemWithIcon layout 2023-07-03 10:24:31 +08:00
tarsin 174e899ead Fix cannot remove localFav 2023-07-03 09:23:28 +08:00
tarsin 84cc915d95 Avoid cascade column 2023-07-03 09:01:57 +08:00
tarsin 7668f568f0 Refactor to use ListItem 2023-07-03 08:38:40 +08:00
tarsin 35a7cc2067 Unify isCronetSupported 2023-07-03 08:09:37 +08:00
tarsin efa0823293 Don't respect cache headers, Resolve #1211 2023-07-03 07:56:28 +08:00
FooIbar 1473f6326d Refactor addToFavorites() 2023-07-03 07:16:44 +08:00
FooIbar 861c05db44 Add option for QUIC support 2023-07-03 07:16:44 +08:00
tarsin 09b92cc709 Reduce allocations 2023-07-02 12:46:18 +08:00
tarsin a2d9113be2 Remove unused defensive code 2023-07-02 12:42:02 +08:00
tarsin a64c7d9f22 Make GalleryTagGroup groupName immutable and nonnull 2023-07-02 12:09:09 +08:00
tarsin 97090a4dc6 Refactor out GalleryTag 2023-07-02 12:01:21 +08:00
tarsin cf2c5b7e82 Drop snapshot maven 2023-07-02 11:25:02 +08:00
tarsin 6ee5b636c2 Upgrade dependencies 2023-07-02 11:04:08 +08:00
tarsin de9fa5be5a Sync rootProjName with repo name 2023-07-02 10:51:29 +08:00
FooIbar ffc3693674 Use the same UA for ehRequest and cronetRequest 2023-07-02 07:20:46 +08:00
FooIbar a3abd224e8 Use interceptor to handle cookies instead 2023-07-02 07:20:46 +08:00
FooIbar ac680afad3 Add cookie header and handle redirect for cronetRequest 2023-07-02 07:20:46 +08:00
FooIbar 52c21319d6 Move error tip below header in gallery detail 2023-07-01 20:00:24 +08:00
FooIbar 7b38171a9d Replace cascade if with when 2023-07-01 20:00:24 +08:00
tarsin c9ef60db67 Regen gradle wrapper 2023-07-01 16:28:20 +08:00
FooIbar aff75c2dec Add option for bypassing Cloudflare 2023-07-01 14:56:27 +08:00
FooIbar 63fcca3c7d Bypass Cloudflare with webview 2023-07-01 14:56:27 +08:00
tarsin 2e3a148c7a Convert to typesafe FilterMode 2023-07-01 12:46:03 +08:00
tarsin a7b239f0b9 Drop unused coroutineScope 2023-07-01 12:21:46 +08:00
tarsin 227cbd6fba Cleanup EhDB nullability 2023-07-01 12:08:18 +08:00
tarsin 8d19dffa01 Ensure Filter DB modification never on main thread 2023-07-01 12:07:23 +08:00
tarsin f3e1400d4c Refine EhFilter API 2023-07-01 11:37:00 +08:00
tarsin ef891ea23d Cleanup EhFilter 2023-07-01 11:29:10 +08:00
tarsin 043a1d8fe7 Cleanup with HtmlCompat.parseAsHtml 2023-07-01 11:21:24 +08:00
tarsin e428695052 Cleanup filter 2023-07-01 11:07:29 +08:00
tarsin 459dd50e91 Cleanup filter 2023-07-01 10:58:36 +08:00
tarsin 4c286a279f Fix FilterScreen nullability 2023-07-01 10:53:36 +08:00
tarsin 88c394480e Upgrade DB, Filter nullability changes 2023-07-01 10:51:13 +08:00
tarsin cdd5585124 Cleanup EhFilter 2023-07-01 10:46:34 +08:00
tarsin b0346b4e9d Revert guava 2023-07-01 10:30:09 +08:00
tarsin beecd6b109 Update r8 2023-07-01 10:08:58 +08:00
tarsin 54d7abd880 Update guava 2023-07-01 10:08:14 +08:00
FooIbar 9f77a4ce0a Drop webp icon since minSdk is 28 2023-07-01 07:12:49 +08:00
renovate[bot] 754a20ee3c Update dependency gradle to v8.2 2023-06-30 22:45:38 +00:00
FooIbar b5b096aeae Add option to force e-hentai thumb server 2023-06-30 17:42:30 +08:00
FooIbar d0ecac07aa Lofi domain is deprecated 2023-06-30 17:42:30 +08:00
FooIbar 67e6387fa8 Fix settings profile cookie since 55b3753 2023-06-30 15:47:17 +08:00
FooIbar bc34f25a3c Exclude some libraries from aboutLibraries 2023-06-30 15:47:17 +08:00
FooIbar afc38c2de8 Add back press handler in select site screen 2023-06-30 15:47:17 +08:00
renovate[bot] c020af3083 Update dependency com.pinterest:ktlint to v0.50.0 2023-06-30 00:41:16 +00:00
renovate[bot] 3f509f8c11 Update dependency com.google.android.material:material to v1.10.0-alpha05 2023-06-29 22:14:41 +00:00
renovate[bot] 75eb25db18 Update chunker to v4 2023-06-30 00:24:09 +08:00
renovate[bot] 79d173cb0d Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.2 2023-06-29 21:43:44 +08:00
tarsin f3eb2c8c17 Optimize GalleryInfo composer path calculation 2023-06-29 19:02:24 +08:00
FooIbar d15685e1ca Add rustfmt and clippy check to CI 2023-06-29 16:06:54 +08:00
renovate[bot] 11535f7d7b Update nav_version to v2.7.0-beta02 2023-06-29 07:53:43 +08:00
FooIbar 0b526040a3 Add interceptor to store cookies for cronet 2023-06-29 07:11:57 +08:00
FooIbar 15696f4e6e Fix duplicate getUConfig() call 2023-06-29 07:11:57 +08:00
FooIbar 0c560518c8 Clean remaining code from 2d20da7 2023-06-29 07:11:57 +08:00
renovate[bot] 302cba9f61 Update ktor to v2.3.2 2023-06-28 19:41:46 +00:00
tarsin 3a33a2425f Cargo fmt 2023-06-28 18:27:22 +08:00
tarsin a5df907bbe Cargo clippy fix 2023-06-28 18:26:29 +08:00
tarsin 6879a02a63 Fix AdvancedSearch pages behaviour 2023-06-28 13:31:35 +08:00
tarsin d86d72212f Fix AdvancedSearch minrating textfield behaviour 2023-06-28 13:21:00 +08:00
tarsin de528ec277 Fix AdvancedSearch minrating textfield style 2023-06-28 12:43:47 +08:00
FooIbar 6f41ddda51 Don't retry on unrecoverable errors 2023-06-27 23:09:57 +08:00
tarsin ffaf8b9c46 Fix SearchLayout insetPadding 2023-06-27 22:18:49 +08:00
tarsin aab1c12aed Fix SearchLayout padding 2023-06-27 22:10:01 +08:00
tarsin 4e427f2761 Add quichint 2023-06-27 22:04:37 +08:00
tarsin 2a6247a89d Cleanup unused res 2023-06-27 22:04:00 +08:00
tarsin 62c3e9fd99 Restore pageRange advanced options 2023-06-27 22:00:17 +08:00
tarsin 1690651428 Restore three more advanced options 2023-06-27 21:44:05 +08:00
tarsin ea95b1c525 Fixup! Restore minRating advanced search 2023-06-27 19:58:29 +08:00
tarsin f67c80a3a3 Restore minRating advanced search 2023-06-27 19:55:15 +08:00
tarsin 6c1ffeacb2 Fixup! Initial compose advanced search 2023-06-27 19:07:26 +08:00
tarsin 753206c2fb Initial compose advanced search 2023-06-27 19:05:59 +08:00
tarsin 2ae8265d05 Fix SearchLayout title font 2023-06-27 18:37:45 +08:00
tarsin 7df7a783f6 Initial migrate SearchLayout to Compose
TODO: advanced options
2023-06-27 18:33:08 +08:00
tarsin 2829d081cc Cleanup with bundleOf ext 2023-06-27 12:20:05 +08:00
tarsin bb3a88813b Cleanup BaseScene 2023-06-27 12:00:27 +08:00
tarsin 2da8cfde01 Unify MainActivity nav behaviour 2023-06-27 11:54:09 +08:00
tarsin 2e23167e94 Enable httpcache for cronet 2023-06-24 19:03:16 +08:00
tarsin 9131f6f8cb Cleanup ListUrlBuilder 2023-06-24 16:48:19 +08:00
tarsin a77a1fd3e1 Drop UrlBuilder 2023-06-24 16:15:52 +08:00
tarsin 9dfc1324ad Flush cookies after signin 2023-06-24 16:05:35 +08:00
tarsin c84e86a5a1 Cleanup HttpUrl usage 2023-06-24 16:02:18 +08:00
tarsin be867d5d07 Cleanup HttpUrl usage 2023-06-24 15:49:00 +08:00
tarsin 5457044dd5 Cleanup a HttpUrl usage 2023-06-24 15:44:29 +08:00
tarsin be2330f1b6 Prepare to drop UrlBuilder 2023-06-24 15:36:31 +08:00
tarsin eb18e74884 Prepare to drop UrlBuilder 2023-06-24 15:28:37 +08:00
renovate[bot] fd05d03bca Update plugin com.mikepenz.aboutlibraries.plugin to v10.8.0 2023-06-24 07:02:05 +08:00
renovate[bot] 69db5bd400 Update dependency com.mikepenz:aboutlibraries-core to v10.8.0 2023-06-23 22:00:15 +00:00
FooIbar a87d720ae3 Support tag suggestion under operators and qualifiers 2023-06-22 18:51:27 +08:00
FooIbar 7b81a9aa50 Cleanup code 2023-06-22 15:40:20 +08:00
FooIbar b07fd2bc4f Remove all unnecessary jvm annotations 2023-06-22 15:40:20 +08:00
FooIbar 3c64486c3b Convert all unifile classes to kotlin 2023-06-22 15:40:20 +08:00
FooIbar 8cb4045589 Rename .java to .kt 2023-06-22 15:40:20 +08:00
FooIbar 7edac11d53 Rename UniFile.kt to UniFileExtensions.kt 2023-06-22 15:40:20 +08:00
tarsin 838f8ac98c Fix galleryTag layout 2023-06-22 11:39:27 +08:00
tarsin 3a57d43aa4 Fixup! Kotlin 1.9.0-RC 2023-06-22 10:26:42 +08:00
tarsin 69870803f7 Kotlin 1.9.0-RC 2023-06-22 10:23:27 +08:00
renovate[bot] eef5e5485a Update dependency androidx.paging:paging-compose to v3.2.0-rc01 2023-06-22 10:03:31 +08:00
renovate[bot] ebbe26e5e6 Update room_version to v2.6.0-alpha02 2023-06-22 10:01:56 +08:00
FooIbar 0cd83d3e4c Convert all legacy view classes to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 1c07c61307 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar b602fa1204 Convert AddDeleteDrawable, HandlerDrawable and WrapDrawable to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 273bed9ba1 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 028405ecce Convert MarginItemDecoration, SimpleSmoothScroller and WindowInsetsAnimationHelper to kotlin 2023-06-22 10:01:41 +08:00
FooIbar ada4158c8e Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar fc99c611a6 Convert ViewTransition and BringOutTransition to kotlin 2023-06-22 10:01:41 +08:00
FooIbar fce4a2751a Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 557818e59f Drop DialogWebChromeClient and SimpleHolder 2023-06-22 10:01:41 +08:00
FooIbar a85f3cbe0b Do ktlint format 2023-06-22 10:01:41 +08:00
FooIbar 5d8acd9577 Drop ViewUtils 2023-06-22 10:01:41 +08:00
FooIbar e2bf887b94 Convert RadioGridGroup to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 4a0893c573 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 839d074df1 Drop NumberUtils 2023-06-22 10:01:41 +08:00
FooIbar 9be7705050 Convert EasyRecyclerView to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 470a2baa6a Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar fb8de517a0 Drop LayoutUtils 2023-06-22 10:01:41 +08:00
FooIbar 635de44299 Convert AboveSnackerLayout to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 1d4bd9a294 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar a0b48a5cc7 Drop MathUtils 2023-06-22 10:01:41 +08:00
FooIbar eb726ccbf4 Convert SimpleGridLayout to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 3f484a49e8 Drop StringUtils and Utilities 2023-06-22 10:01:41 +08:00
FooIbar 09cd08bfaa Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar e6312a37c7 Convert LayoutManagerUtils to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 862b3e5e46 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 0042482107 Convert SimpleHandler to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 85bc215e00 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 2db5a749a0 Drop IOUtils 2023-06-22 10:01:41 +08:00
FooIbar 8c439b1167 Convert OSUtils to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 4eb1a4141f Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 4f47febc06 Convert AnimationUtils, ConcurrentPool, IntIdGenerator and SimpleAnimatorListener to kotlin 2023-06-22 10:01:41 +08:00
FooIbar c61afc7a04 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar b59966e8a4 Convert FastScroller to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 457cf2e856 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 4a7626c6f6 Convert FileUtils to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 947452e3f1 Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar b6de84c369 Drop ObjectUtils 2023-06-22 10:01:41 +08:00
FooIbar 0adc43b243 Convert ReadableTime and TextUrl to kotlin 2023-06-22 10:01:41 +08:00
FooIbar 0c9a8624bc Rename .java to .kt 2023-06-22 10:01:41 +08:00
FooIbar 09e9d3ff5a Drop JsoupUtils and LogCat 2023-06-22 10:01:41 +08:00
renovate[bot] 7240ca4eba Update dependency androidx.activity:activity-compose to v1.8.0-alpha06 2023-06-22 00:41:41 +00:00
tarsin 0a2c29a693 Adjust GalleryInfo layout 2023-06-21 10:37:05 +08:00
tarsin bba875238c Rewrite GalleryInfo layout with ConstraintLayout dsl 2023-06-21 10:28:16 +08:00
FooIbar c33248b1bf Use file hash for cover search 2023-06-21 00:52:28 +08:00
FooIbar 24431051ae Make HV event notification clickable 2023-06-20 23:03:59 +08:00
FooIbar 1758e25388 Cleanup code 2023-06-20 23:03:59 +08:00
tarsin a27e8b99d9 Fix parse e-hentai failed 2023-06-20 22:18:19 +08:00
tarsin a06e76e934 Fix proguard and do cargo fmt 2023-06-20 21:53:48 +08:00
FooIbar 14609583b5 Show favorite name in gallery list 2023-06-20 21:49:28 +08:00
FooIbar 751a89c107 Refine rust gallery list parser 2023-06-20 21:49:28 +08:00
tarsin 619c8f6deb Update r8 2023-06-20 19:54:12 +08:00
tarsin c3835b8a65 Drop unused field in GalleryInfo 2023-06-20 19:43:42 +08:00
tarsin 5fd890cf9b Cleanup code 2023-06-20 19:40:32 +08:00
tarsin 40c6135214 Cleanup code 2023-06-20 19:37:06 +08:00
tarsin cfa1ce8ab6 Cleanup code 2023-06-20 19:34:38 +08:00
tarsin 260dafcd79 Fixup bytebuffer usage 2023-06-20 19:27:12 +08:00
tarsin 73b3cd4625 Add annotations 2023-06-20 17:36:44 +08:00
tarsin d895a07583 Cleanup code 2023-06-20 17:33:48 +08:00
tarsin b84872aca2 Format code 2023-06-20 17:16:41 +08:00
tarsin 0ef4bccb7c Intercept okhttp request to cronet 2023-06-20 17:15:48 +08:00
tarsin dfc6c6a030 Let okhttp cronet converter handle cookies 2023-06-20 17:15:26 +08:00
tarsin b62f1d0c9f Add copy of okhttp cronet converter 2023-06-20 17:02:02 +08:00
tarsin 8abb5d9fb4 Add SwitchPref switch padding 2023-06-20 12:24:33 +08:00
tarsin 5f13725173 Handle #1157 [2/2] 2023-06-20 11:55:25 +08:00
tarsin 7f8b41f998 Handle #1157 [1/2] 2023-06-20 11:51:46 +08:00
FooIbar 1319060311 Drop some ViewUtils methods 2023-06-20 00:51:49 +08:00
FooIbar d1f914c2fa Convert SearchLayout to view binding 2023-06-20 00:51:49 +08:00
FooIbar 46cd1013fb Convert GalleryDetailScene to view binding 2023-06-20 00:51:49 +08:00
FooIbar 2ed516cf85 Convert SuggestionHolder to view binding 2023-06-20 00:51:49 +08:00
FooIbar 2ce55b4aef Convert GalleryCommentsScene to view binding 2023-06-20 00:51:49 +08:00
FooIbar df7071a22c Convert FavoritesScene to view binding 2023-06-20 00:51:49 +08:00
tarsin 707529972e Cleanup code 2023-06-19 11:28:54 +08:00
tarsin cf2796ebaa Add cronet temporarily for testing usage
We temporarily add cronet implementation to test the wrapper stability
Drop it when Android 14 release
2023-06-19 10:38:36 +08:00
tarsin 9611d81969 Include ktor proguard file 2023-06-19 01:37:58 +08:00
tarsin fb31f627a1 Update build.gradle.kts 2023-06-18 17:51:15 +08:00
tarsin 4741e4f858 Validate content length 2023-06-18 16:39:04 +08:00
tarsin b44ddf980b Cleanup code 2023-06-18 16:35:55 +08:00
tarsin 47b97243c3 Refine polling recycle 2023-06-18 16:33:54 +08:00
tarsin b695b06507 Pooling the bytebuffer 2023-06-18 16:30:18 +08:00
tarsin 110fb9fb59 Refine HttpEngineCoroutinizer 2023-06-18 16:13:54 +08:00
tarsin fe7c979d78 Cleanup HttpEngineCoroutinizer 2023-06-18 15:25:19 +08:00
tarsin 0d10899417 Initial support Android14 HttpEngine 2023-06-18 15:09:50 +08:00
tarsin ff89afb8c1 Fix fragment predictive back 2023-06-17 23:40:10 +08:00
tarsin 188248c634 Fix searchview predictive back 2023-06-17 23:30:01 +08:00
tarsin 0d3c942984 Fix navigation drawer predictive back 2023-06-17 23:26:22 +08:00
tarsin 2b520be693 Upgrade androidx activity 2023-06-17 23:22:34 +08:00
tarsin 4b831a2ee1 Support fragment predictive back 2023-06-17 23:19:32 +08:00
tarsin 59037d60e2 Revert "Revert "Revert "Update README.md"""
This reverts commit 61917285d5.
2023-06-17 22:01:57 +08:00
tarsin 61917285d5 Revert "Revert "Update README.md""
This reverts commit 6ccb59957d.
2023-06-17 17:56:45 +08:00
tarsin 453f77dd3e Upgrade androidx fragment 2023-06-17 17:42:18 +08:00
tarsin b40004d9ba Fix download crash on Android 14 2023-06-17 17:37:03 +08:00
tarsin f8f2d020eb Upgrade mdc for searchbar predictive back 2023-06-17 17:32:08 +08:00
tarsin 4f09de8a69 Refine warning 2023-06-17 16:51:37 +08:00
tarsin 2d20da70b8 Drop domain fronting and other network hacks since #1147 2023-06-17 16:37:51 +08:00
tarsin cf95e7b034 Upgrade compileSdk to 34 2023-06-17 15:01:33 +08:00
tarsin b5709a49f4 Fix padding 2023-06-16 20:59:46 +08:00
tarsin 75a7143442 Tweak GalleryDetailScene button layout 2023-06-16 20:57:52 +08:00
tarsin 3e47d82713 Spilt galleryList parser 2023-06-16 16:58:03 +08:00
tarsin 62f9361d25 Cleanup code 2023-06-16 16:52:19 +08:00
tarsin 3a97b93b6d Parse gid and token 2023-06-16 16:48:24 +08:00
tarsin 3b9850b90d Trim title 2023-06-16 16:26:30 +08:00
tarsin c0636e93c6 Refine rust gd parser 2023-06-16 16:22:55 +08:00
tarsin 5ff1131dee Improve home parser, avoid iter over whole text 2023-06-16 15:35:34 +08:00
tarsin 8aca589b0f Cleanup rust gd parser 2023-06-16 15:07:00 +08:00
tarsin 10f6bd4bf6 Refine rust gd parser 2023-06-16 15:01:11 +08:00
tarsin dbe84e8bda Refine rust gd parser 2023-06-16 14:29:44 +08:00
tarsin f4de403cdd Refresh libraries 2023-06-16 12:34:03 +08:00
tarsin 9024afed19 Strip unused regex feature 2023-06-16 12:21:51 +08:00
tarsin d8921377b3 Cleanup rust implementation 2023-06-16 01:35:16 +08:00
tarsin b80deafaa8 Cleanup home.rs 2023-06-16 00:22:54 +08:00
tarsin 506c63380a Spilt home.rs 2023-06-16 00:19:22 +08:00
tarsin 9e4fc2e7af Spilt torrent.rs 2023-06-16 00:16:58 +08:00
tarsin 0a8df26549 Add stub rust BaseGalleryInfo binding 2023-06-16 00:09:35 +08:00
tarsin d2bbd1b842 Tweak BaseGalleryInfo layout 2023-06-16 00:07:38 +08:00
tarsin 5974ce11a1 Build arm64 only if debug 2023-06-15 22:47:28 +08:00
tarsin 3ec700da56 Add f32 jni binding support 2023-06-15 20:33:14 +08:00
tarsin cb7b601f58 Revert "Drop fromJava impl and unused std impl"
This reverts commit 8e08a4881f.
2023-06-15 18:25:18 +08:00
tarsin 8e08a4881f Drop fromJava impl and unused std impl 2023-06-15 17:41:43 +08:00
tarsin 2fac1c8e4c Step to globally update to jni-rs 0.20.0 2023-06-15 15:43:37 +08:00
tarsin d240fb7e05 Add jnix for jni 0.21 upgrade 2023-06-15 14:43:50 +08:00
tarsin 326d378979 Cleanup package 2023-06-15 14:36:47 +08:00
tarsin 71f91bf054 Obtain jni_fn locally 2023-06-15 13:50:25 +08:00
tarsin 40e4d40683 Fix dependencies 2023-06-15 13:44:58 +08:00
tarsin c9d439e748 Obtain catch_panic, catch_panic_macros locally 2023-06-15 13:32:33 +08:00
tarsin 0ec90655c0 Specific jni-rs version 2023-06-15 13:13:47 +08:00
tarsin c6949cd09b Update rustup before compile 2023-06-15 13:00:32 +08:00
tarsin 1e47170c3b Refine rust jni parseLimit 2023-06-15 12:22:30 +08:00
tarsin b7d2e4e5da Cleanup code 2023-06-15 12:04:37 +08:00
tarsin 28c7a701ca Use jni signature generator 2023-06-15 11:56:57 +08:00
tarsin 52560b6b95 Catch rust panic 2023-06-15 11:52:02 +08:00
tarsin 1c2c18bff1 Cleanup rust binding 2023-06-15 05:06:12 +08:00
tarsin ed1603eec4 Update proguard-rules.pro 2023-06-15 04:25:36 +08:00
tarsin 2206dd85bc Use jnix 2023-06-15 04:13:01 +08:00
tarsin 62509e1500 Use lazy regex! marco 2023-06-14 20:59:11 +08:00
tarsin 56cad66aec Tweak cargo profile 2023-06-14 16:52:54 +08:00
tarsin 660d917f97 Unescape xml for favcat name 2023-06-14 16:34:10 +08:00
tarsin 0ca5572684 Do rustfmt, unescape xml 2023-06-14 16:29:39 +08:00
tarsin cbebdb542d Move torrent parser to rust 2023-06-14 16:20:18 +08:00
tarsin 84d13fe9c5 Cleanup rust code 2023-06-14 13:05:01 +08:00
tarsin c6919c39c6 Move favParser to rust 2023-06-14 12:41:44 +08:00
tarsin 92b6069654 Cleanup rust parser 2023-06-14 11:36:36 +08:00
tarsin 804dffc81a Cleanup rust parser 2023-06-14 10:30:07 +08:00
tarsin 5831761fc0 Drop logger for now 2023-06-12 15:42:42 +08:00
tarsin c887379b8b Ignore Cargo.lock as well 2023-06-12 15:39:35 +08:00
tarsin 82c92203b6 Initial refactor HomeParser with rust 2023-06-12 15:36:58 +08:00
tarsin 881948554e Cleanup buildscript 2023-06-12 12:05:05 +08:00
tarsin b0357201a5 Downgrade AGP to stable version 2023-06-12 11:58:48 +08:00
tarsin 7ddeaf6d16 Use cdylib and release profile for now 2023-06-12 11:07:11 +08:00
tarsin 942abd7dbb Add support for build rust dylib 2023-06-12 11:01:18 +08:00
tarsin 9f78b69390 Resolve #1136 2023-06-11 12:36:11 +08:00
tarsin 6e561ca9a9 Update r8 2023-06-10 17:15:55 +08:00
tarsin cec86576c1 Singletonize ReaderPreferences 2023-06-10 13:56:18 +08:00
tarsin 9eb8e984b6 Add chunker http inspector for debug usage 2023-06-09 19:00:03 +08:00
renovate[bot] 20798b5d25 Update plugin com.android.application to v8.1.0-beta05 2023-06-09 01:45:54 +00:00
tarsin 29fafdf216 Upgrade paging 2023-06-08 16:53:32 +08:00
tarsin 2fbda42e53 Cleanup CookiesDao 2023-06-08 16:26:49 +08:00
tarsin 285feb5f5b Cleanup EhCookieStore 2023-06-08 16:22:36 +08:00
tarsin cfdb8793c6 Cleanup EhCookieStore 2023-06-08 16:18:32 +08:00
tarsin 55b3753e66 Migrate cookies store to CookieManager backend 2023-06-08 16:11:48 +08:00
renovate[bot] ad04681460 Update dependency androidx.paging:paging-common-ktx to v3.2.0-beta01 2023-06-08 04:07:38 +00:00
renovate[bot] 2e132f06b7 Update nav_version to v2.7.0-beta01 2023-06-08 09:30:00 +08:00
renovate[bot] dc74266927 Update dependency androidx.core:core-ktx to v1.11.0-beta02 2023-06-07 21:39:55 +00:00
tarsin e002e15a66 Fix imagelimits dialog crash 2023-06-07 16:14:10 +08:00
tarsin e149c7065e Upgrade r8 2023-06-07 15:46:50 +08:00
tarsin 107a5ce626 Tweak package layout 2023-06-07 15:39:35 +08:00
tarsin b6d69f7a02 Tweak package layout 2023-06-07 15:36:56 +08:00
tarsin 6de6c89439 Tweak package layout 2023-06-07 15:35:16 +08:00
tarsin c1ca6dcd5d Tweak package layout 2023-06-07 15:31:33 +08:00
tarsin 223ebd6acd Flatten legacy view package 2023-06-07 15:28:47 +08:00
tarsin cc8311e116 Opt-in DataStorePreferencesPreview 2023-06-07 15:18:58 +08:00
tarsin a66f53e562 Trick to fix amoled mode 2023-06-07 15:13:08 +08:00
tarsin de699dacfe Subscribe show_tag_translations changes 2023-06-07 15:04:36 +08:00
tarsin 431007a731 Subscribe gallerysite changes 2023-06-07 15:02:24 +08:00
tarsin 797d511c2c Protect preferences need certain locales 2023-06-07 14:58:22 +08:00
tarsin 0c40be5da8 Protect preferences need signin 2023-06-07 14:55:33 +08:00
tarsin 2fc04caa05 Drop unused res 2023-06-07 14:48:23 +08:00
tarsin 78ffe38f54 Fix securityDelay pref summary 2023-06-07 14:45:19 +08:00
tarsin 8152a04155 Fix thumbResolution pref summary 2023-06-07 14:38:07 +08:00
tarsin 64b39275e2 Restore UConfigScreen applyTip 2023-06-07 14:33:21 +08:00
tarsin 23c6291cc8 Use mutableStateListOf, remove some recompose trigger 2023-06-07 14:29:22 +08:00
tarsin 60fe67b97a Restore filter tip 2023-06-07 14:22:28 +08:00
tarsin 96264a28a6 Fixup! Restore dialog to add filter 2023-06-07 13:51:47 +08:00
tarsin b86cb676d6 Restore dialog to add filter 2023-06-07 13:43:57 +08:00
tarsin a05fc8447d Restore dialog to delete filter 2023-06-07 13:28:45 +08:00
tarsin 68a617b952 FilterScreen optimize recompose 2023-06-06 23:48:14 +08:00
tarsin f18bfab35f FilterScreen appbar liftonScroll 2023-06-06 23:45:11 +08:00
tarsin c04f30a5b7 FilterScreen implementation part 4 2023-06-06 23:43:51 +08:00
tarsin 7a35d60b71 FilterScreen implementation part 3 2023-06-06 23:40:42 +08:00
tarsin cd3fa38161 FilterScreen implementation part 2 2023-06-06 23:34:33 +08:00
tarsin 6a65eabf28 FilterScreen implementation part 1 2023-06-06 23:29:55 +08:00
tarsin 0905a4dfba FilterScreen help message 2023-06-06 23:08:42 +08:00
tarsin c68c011e69 FilterScreen stub 2023-06-06 23:05:51 +08:00
tarsin a57066b6db Implement MyTagsScreen 2023-06-06 23:01:24 +08:00
tarsin 0b19cc71c9 Cleanup UConfigScreen 2023-06-06 22:48:55 +08:00
tarsin 38e6b60005 Cleanup UConfigScreen 2023-06-06 22:36:34 +08:00
tarsin 35a18ecc0f Navback after settings applied 2023-06-06 22:34:12 +08:00
tarsin 5fb2ec2fda Cleanup js apply 2023-06-06 22:31:27 +08:00
tarsin 61a2ae9431 Implement UConfigScreen 2023-06-06 22:23:36 +08:00
tarsin cba05171af Convert to use accompanist Webview 2023-06-06 21:54:25 +08:00
renovate[bot] 816d63e432 Update accompanist_version to v0.31.3-SNAPSHOT 2023-06-06 21:41:40 +08:00
tarsin 56c2e6fcde Import accompanist webview 2023-06-06 21:39:34 +08:00
tarsin d6aee24295 Remove Activity ref 2023-06-06 19:49:42 +08:00
tarsin 13b28c0f38 Introduce FINISH_ROUTE_NAME 2023-06-06 19:47:08 +08:00
tarsin 2f69b942d9 Cleanup SelectSiteScreen 2023-06-06 19:11:07 +08:00
tarsin 5977d77381 Navigate to SIGN_IN_ROUTE directly when signout 2023-06-06 19:04:01 +08:00
tarsin fe336ebf6a Fixup merge 2023-06-06 19:00:12 +08:00
tarsin c44776870e Merge LoginActivity & SettingsActivity 2023-06-06 18:54:29 +08:00
tarsin 67d5942aff Observe updateWhenRequestNewsChanges 2023-06-06 16:36:00 +08:00
tarsin 48e3b44658 Implement compose TimePickerDialog 2023-06-06 16:30:59 +08:00
tarsin 89347768b3 Fix codestyle 2023-06-06 15:50:33 +08:00
tarsin 2ed726427f Add AnimatedVisibility for preference dependency 2023-06-06 15:49:37 +08:00
tarsin 8ee20070b6 Add FilterScreen stub 2023-06-06 15:45:35 +08:00
tarsin ca9e1e93d9 Add MyTagsScreen stub 2023-06-06 15:43:28 +08:00
tarsin d9ea549b51 Add UConfigScreen stub 2023-06-06 15:38:48 +08:00
tarsin 0860bb488f [DO NOT USE] Restore ability to reset image limits 2023-06-06 15:23:53 +08:00
tarsin 8efc5401cb [DO NOT USE] Show Integer in slider 2023-06-06 14:36:29 +08:00
tarsin 13e5649948 [DO NOT USE] Add 2 missing preference 2023-06-06 14:33:08 +08:00
tarsin bb77c4ecfc [DO NOT USE] Introduce LauncherPreference 2023-06-06 14:06:07 +08:00
tarsin 3d16151bd2 [DO NOT USE] Fixup codestyle 2023-06-06 13:40:58 +08:00
tarsin 9053b200b2 [DO NOT USE] Restore copy notify 2023-06-06 01:24:31 +08:00
tarsin a450dd29f9 [DO NOT USE] Cleanup AccountPreference 2023-06-06 01:18:04 +08:00
tarsin 2f309fe9bb [DO NOT USE] Cleanup AccountPreference 2023-06-06 01:16:34 +08:00
tarsin 5f9f7d0ba7 [DO NOT USE] Restore AccountPreference 2023-06-06 01:13:59 +08:00
tarsin 6f1a08f2c7 [DO NOT USE] Introduce WorkPreference 2023-06-06 00:46:33 +08:00
tarsin 563f6810be [DO NOT USE] Merge object PreferenceTokens 2023-06-06 00:39:14 +08:00
tarsin 66ed7a04a4 [DO NOT USE] Cleanup preference default args 2023-06-06 00:38:04 +08:00
tarsin 626475a91f [DO NOT USE] Restore ability to clear redundancy 2023-06-06 00:34:11 +08:00
tarsin f99575771c [DO NOT USE] Restore ability to restore downloads 2023-06-06 00:28:21 +08:00
tarsin bc5a0f9140 [DO NOT USE] Fix to use Transition.None 2023-06-05 23:52:35 +08:00
tarsin 40958179ca [DO NOT USE] Use new navigation anim 2023-06-05 23:45:54 +08:00
tarsin d98006c2cd [DO NOT USE] Upgrade androidx navigation to 2.7.0-alpha01 2023-06-05 21:28:43 +08:00
tarsin 5134c9b378 [DO NOT USE] Observe theme changed 2023-06-05 19:04:31 +08:00
tarsin 1379953a9c [DO NOT USE] Inline setting keys 2023-06-05 18:45:14 +08:00
tarsin cc02b69020 [DO NOT USE] EhScreen implementation part 1 2023-06-05 18:42:16 +08:00
tarsin 55b4edac4d [DO NOT USE] Cleanup unused res and code 2023-06-05 18:18:27 +08:00
tarsin b56f136da0 [DO NOT USE] Introduce MutableState<T>.rememberedAccessor 2023-06-05 17:49:49 +08:00
tarsin d2b5e5ec01 [DO NOT USE] Tweak SimpleMenuPreferenceInt to receive a MutableState 2023-06-05 17:10:39 +08:00
tarsin 623ab63d8d [DO NOT USE] Introduce KMutableProperty0<T>.observed 2023-06-05 16:58:38 +08:00
tarsin 3b1beba570 [DO NOT USE] Implement DownloadScreen part 4 2023-06-05 16:05:38 +08:00
tarsin 4e6210c1f8 [DO NOT USE] Implement DownloadScreen part 3 2023-06-05 16:00:50 +08:00
tarsin 281154be7a [DO NOT USE] Implement DownloadScreen part 2 2023-06-05 15:55:07 +08:00
tarsin 9b4477766c [DO NOT USE] Implement DownloadScreen part 1 2023-06-05 15:45:02 +08:00
tarsin 53bbabab75 [DO NOT USE] Drop in-app proxy support 2023-06-05 15:32:51 +08:00
tarsin b1671eacb7 [DO NOT USE] Restore DOH support 2023-06-05 15:28:00 +08:00
tarsin b41e564411 [DO NOT USE] Import backupFav 2023-06-05 15:23:24 +08:00
tarsin 48e12154c3 [DO NOT USE] Fix db import 2023-06-05 15:17:05 +08:00
tarsin 5c25e2801c [DO NOT USE] Implement DB import 2023-06-05 14:51:12 +08:00
tarsin c2d2c1c4f6 [DO NOT USE] Implement DB export 2023-06-05 14:45:34 +08:00
tarsin 59959bb9ee [DO NOT USE] Migrate to datastore backend 2023-06-05 12:24:08 +08:00
tarsin 3664ca846c [DO NOT USE] Drop androidx preference, drop rikkax preference 2023-06-05 12:17:55 +08:00
tarsin 8c6d28d6e2 Make AndroidPreferenceStore use Settings backend 2023-06-05 12:09:36 +08:00
tarsin b3acd1d603 Implement openByDefault 2023-06-05 11:57:53 +08:00
tarsin d76e056b91 Implement dumpLogcatLauncher 2023-06-05 11:50:16 +08:00
tarsin 5de5dbabef AdvancedScreen placeholder 2023-06-05 11:19:09 +08:00
tarsin 866ea6369e Refine observed usage 2023-06-05 01:14:53 +08:00
tarsin ae2f7204fa Implement Compose SimpleMenuPreference 2 2023-06-05 01:09:41 +08:00
tarsin 5be2b77e42 Implement Compose SimpleMenuPreference 2023-06-05 00:55:49 +08:00
tarsin 8f1f76c3d1 Import DropDownPref 2023-06-05 00:31:56 +08:00
tarsin 6f488c1d00 Implement part of AdvancedScreen 2023-06-05 00:27:00 +08:00
tarsin ee7724e754 Create EhScreen, DownloadScreen stub 2023-06-05 00:19:18 +08:00
tarsin 6c4848b6f9 Apply applyPopAnimationsToPendingTransition globally 2023-06-05 00:15:31 +08:00
tarsin b6c36c20b6 Add stub AdvancedScreen 2023-06-05 00:13:21 +08:00
tarsin a29fc4ce94 Fix SliderPref enable status 2023-06-05 00:10:30 +08:00
tarsin 6490598fc3 Add IntSliderPreference 2023-06-05 00:05:25 +08:00
tarsin f3519f6f8a Import SliderPref 2023-06-04 23:53:42 +08:00
tarsin d44192079d Properly implement Preference 2023-06-04 23:39:33 +08:00
tarsin 140fac4f3b Stupid implementation of SwitchPreference 2023-06-04 17:33:01 +08:00
tarsin 802db256c1 Cleanup LicenseScreen 2023-06-04 16:58:10 +08:00
tarsin c8a9cfa8a3 Add SecurityScreen stub 2023-06-04 16:56:39 +08:00
tarsin 2213969c6e Fixup LicenseScreen padding 2023-06-04 16:51:45 +08:00
tarsin 524a3a94c4 Cleanup AboutScreen 2023-06-04 16:49:16 +08:00
tarsin 4ec58a0f62 Add standalone SpannedHelper 2023-06-04 16:43:01 +08:00
tarsin e25227e39d Add nestedScrollConnection 2023-06-04 16:38:28 +08:00
tarsin 7efaa64562 Implement AboutScreen 2023-06-04 16:30:49 +08:00
tarsin a2b95f6c61 AboutScreen stub 2023-06-04 15:32:32 +08:00
tarsin 9a6aa1a591 Cleanup PreferenceHeader implementation 2023-06-04 15:28:36 +08:00
tarsin de780635e4 Implement BaseScreen 2023-06-04 15:25:30 +08:00
tarsin 3dbb40967f Introduce BaseScreen 2023-06-04 14:54:59 +08:00
tarsin f39d17abc0 Introduce new compose SettingsActivity stub 2023-06-04 14:42:56 +08:00
tarsin a015e54569 Revert "Introduce KTLS based http downoader API stub"
This reverts commit a256864c84.
2023-06-03 22:16:51 +08:00
tarsin a256864c84 Introduce KTLS based http downoader API stub 2023-06-03 17:37:53 +08:00
tarsin d74b5bff7b Konvert AppConfig 2023-06-03 15:40:01 +08:00
tarsin 31e7943d22 Rename .java to .kt 2023-06-03 15:40:01 +08:00
tarsin b2d1660e95 Drop GetText 2023-06-03 15:06:42 +08:00
tarsin 5d13e29300 Cleanup unused res 2023-06-03 15:00:06 +08:00
tarsin aaf9ef06dd Rewrite ProgressScene with Jetpack Compose 2023-06-02 21:23:30 +08:00
tarsin 6725ef992a Drop Settings.initialize 2023-06-02 18:14:00 +08:00
tarsin b5baecf8d0 Singletonize EhProxySelector 2023-06-02 17:58:35 +08:00
tarsin 58d1ace612 Singletonize FavouriteStatusRouter 2023-06-02 17:55:40 +08:00
tarsin a4eca58275 Don't opt-in InternalCoroutinesApi 2023-06-02 15:53:18 +08:00
tarsin c97974eb72 Purify Settings 2023-06-02 15:47:11 +08:00
tarsin c46c386fea Move downloadLocation getter setter companion of DownloadManager 2023-06-02 15:43:10 +08:00
tarsin 3f4ca1bcbf Cleanup Settings code style 2023-06-02 15:35:34 +08:00
tarsin e4e04a4508 Cleanup Settings 2023-06-02 15:34:01 +08:00
tarsin 6d87dc0b1c Continue to cleanup Settings 2023-06-02 15:21:58 +08:00
tarsin db16a0c17c Cleanup Settings 2023-06-02 15:20:41 +08:00
tarsin 8fc411a30b Make Delegate private interface 2023-06-02 15:13:37 +08:00
tarsin c4a15f8e00 Optin ExperimentalSplittiesApi 2023-06-02 15:12:18 +08:00
tarsin 90e6918db0 Cleanup Settings 2023-06-02 15:09:40 +08:00
tarsin 62d7d09886 Inline launchPageGalleryListSceneAction 2023-06-02 15:03:34 +08:00
tarsin d9c933bd0a Inline thumbSize 2023-06-02 14:59:46 +08:00
tarsin 06c3e3c8a0 Fix downloadLocation scheme 2023-06-02 14:54:15 +08:00
tarsin f284ac71b5 Introduce stringArrayPref 2023-06-02 14:47:56 +08:00
tarsin cd969ac33b Introduce intArrayPref 2023-06-02 14:38:20 +08:00
tarsin ac43df5e03 Refine saveFromHttpResponse, avoid a buffer allocation 2023-06-02 14:14:21 +08:00
tarsin 8ea6417ea6 Refactor out interceptSecurityOrReturn 2023-06-02 13:49:49 +08:00
tarsin 19295e1d30 Refactor out lockObserver 2023-06-02 13:46:15 +08:00
tarsin f84b10f2eb Resolve #1124 2023-06-02 12:31:11 +08:00
tarsin 184edf9d4c Use splitties roomDb builder 2023-06-02 12:19:04 +08:00
tarsin 257e247e23 Inline listThumbSize 2023-06-01 21:10:47 +08:00
tarsin a2f0b4a64f Inline putPasswdToArchivePasswds 2023-06-01 21:08:26 +08:00
tarsin 9f1ec2be7e Inline intFrom 2023-06-01 21:05:22 +08:00
tarsin 046ba62256 Introduce intFromStrPref 2023-06-01 21:04:03 +08:00
tarsin c9f18b0b71 Convert to use intFromStrPref 2023-06-01 19:34:05 +08:00
tarsin b61e1bdbe3 Introduce intFromStrPref 2023-06-01 19:17:23 +08:00
tarsin 1a01caee2c Cleanup archivePasswds pref 2023-06-01 18:48:33 +08:00
tarsin 2fed610470 Cleanup listThumbSize pref 2023-06-01 18:39:36 +08:00
tarsin 2026843628 Inline some preference keys 2023-06-01 17:49:27 +08:00
tarsin 9997d31421 Inline some preference keys 2023-06-01 17:46:36 +08:00
tarsin 1079e4a759 Sort preferences by type 2023-06-01 17:40:44 +08:00
tarsin ce5c258cf9 Move some property to delegate 2023-06-01 17:36:06 +08:00
tarsin c1b76b35ec Move some property to delegate 2023-06-01 17:28:22 +08:00
tarsin e9cb083672 Move some property to delegate 2023-06-01 17:16:23 +08:00
tarsin 034a5c2071 Move some property to delegate 2023-06-01 17:07:57 +08:00
tarsin 6c7e3019ff Import splitties preferences 2023-06-01 16:56:23 +08:00
tarsin fc02902ccb Restore LongClick HapticFeedback 2023-06-01 16:17:59 +08:00
tarsin f33ff6d380 Use Splitties systemServices 2023-06-01 16:13:37 +08:00
tarsin 2c30deb298 Use Splitties appCtx 2023-06-01 15:48:45 +08:00
tarsin 6ea635a1e9 Resolve #1122 2023-05-31 23:21:59 +08:00
tarsin 858134927a Resolve #1122 2023-05-31 21:38:52 +08:00
tarsin 0eec51ef8e Revert "Resolve #1122"
This reverts commit de065076df.
2023-05-31 21:28:45 +08:00
tarsin de065076df Resolve #1122 2023-05-31 18:51:22 +08:00
tarsin ad4c418736 Resolve #1121 2023-05-31 18:37:45 +08:00
tarsin 9ab368a760 Workaround download in UIContext 2023-05-31 18:00:50 +08:00
tarsin 59f3cf20a5 Introduce DialogState.doGalleryInfoAction 2023-05-31 17:57:24 +08:00
tarsin 8a58b6ee0f Move to DialogState.showMoveDownloadLabel impl 2023-05-31 17:45:28 +08:00
tarsin d6816c89e5 Refactor compose dialog 2023-05-31 17:02:32 +08:00
tarsin 7ca37ee54a Tweak prefetchDistance 2023-05-31 14:46:32 +08:00
tarsin 4b4304629f Fix previewScene jump 2023-05-31 14:36:32 +08:00
tarsin dba557b942 Fix codestyle 2023-05-31 13:50:37 +08:00
tarsin fe158e7f62 Tweak DialogScope 2023-05-31 13:47:53 +08:00
tarsin ac7fed649a Implement dialogState.confirmRemoveDownload 2023-05-31 13:25:23 +08:00
tarsin cb67a51686 Move DialogState.selectGalleryInfoAction to CommonOperations 2023-05-31 12:42:50 +08:00
tarsin 4d85a2aa41 Resolve #1120 2023-05-31 12:29:29 +08:00
tarsin 0df875d65a Upgrade AGP 2023-05-31 12:17:06 +08:00
tarsin fff1ec0947 Downgrade AGP to stable version 2023-05-31 12:06:14 +08:00
tarsin 250a7ef25e Update build.gradle.kts 2023-05-31 02:16:30 +08:00
tarsin 805d22802c Update build.gradle.kts 2023-05-31 02:14:20 +08:00
tarsin 9745ce7d34 Update build.gradle.kts 2023-05-31 01:55:27 +08:00
tarsin c165c15099 Upgrade AGP 2023-05-31 01:28:03 +08:00
tarsin 351231aab9 Delegate all reader call to Context.navToReader 2023-05-31 01:10:08 +08:00
tarsin 0e78494f39 Cleanup code 2023-05-31 00:52:13 +08:00
tarsin 886ce38543 Drop SelectItemWithIconAdapter 2023-05-31 00:46:52 +08:00
tarsin f542c8a12e Inline NoHistory 2023-05-31 00:27:07 +08:00
tarsin c35cac860f Cleanup showSelectItemWithIcon implementation 2023-05-31 00:25:24 +08:00
tarsin afcdc5ec5b Fix typo 2023-05-30 23:58:04 +08:00
tarsin b9f24001ed Move to showSelectItemWithIcon impl 2023-05-30 23:55:40 +08:00
tarsin 1f0319c242 Tweak showSelectItemWithIcon style 2023-05-30 23:49:29 +08:00
tarsin 5e8e95f716 Introduce DialogScope.showSelectItemWithIcon 2023-05-30 23:41:19 +08:00
tarsin dd73b20b3f Konvert ShortcutsActivity 2023-05-30 21:41:59 +08:00
tarsin c6a83ac6b2 Rename .java to .kt 2023-05-30 21:41:59 +08:00
tarsin 6ea7a1405f Cleanup unused code 2023-05-30 21:37:47 +08:00
tarsin 774db8d3a4 Always merge requests when preloadThumbAggressively 2023-05-30 21:26:19 +08:00
tarsin a7187d62d3 Konvert ImageLoader.Builder 2023-05-30 21:24:36 +08:00
tarsin e5788b9ec5 Cleanup EhApplication 2023-05-30 21:20:36 +08:00
tarsin be96491091 Introduce idiomatic OkHttp builders 2023-05-30 21:17:36 +08:00
tarsin ea0b83476f Konvert FavouriteStatusRouter 2023-05-30 20:41:14 +08:00
tarsin 187f77d32f Rename .java to .kt 2023-05-30 20:41:14 +08:00
tarsin 46655e3326 Move builtInHosts to Standalone EhHostsConfig 2023-05-30 20:35:32 +08:00
tarsin 4f1615b736 Cleanup EhRequestBuilder 2023-05-30 20:30:50 +08:00
tarsin 8b3ab44c1c Cleanup EhSSLSocketFactory 2023-05-30 20:27:45 +08:00
tarsin 10b6258175 Only make large blob downloading sensitive for cancelling 2023-05-30 20:17:40 +08:00
tarsin 13b532c12a Revert "Fix usingCancellable with null ret value"
This reverts commit d464d00067.
2023-05-30 20:16:02 +08:00
tarsin 126f4238d6 Fix DownloadsScene lag 2023-05-30 18:18:30 +08:00
tarsin 9666feac57 Cleanup downloadManager ref in DownloadsScene 2023-05-30 17:43:57 +08:00
tarsin d464d00067 Fix usingCancellable with null ret value 2023-05-29 20:04:15 +08:00
tarsin 6e96571cc8 Cleanup UrlBuilder with HttpUrl 2023-05-29 19:58:56 +08:00
tarsin 2b70c983bf Document Call.usingCancellable 2023-05-29 17:41:59 +08:00
tarsin 3a2c18d8a9 Refine saveFromHttpResponse 2023-05-29 17:24:34 +08:00
tarsin 29fb40eecc Introduce Call.usingCancellable 2023-05-29 17:02:46 +08:00
tarsin 684d25780e Introduce runInterruptibleOkio 2023-05-29 16:03:41 +08:00
tarsin 51a292e5d0 Use runInterruptible for more sensitive cancellation 2023-05-29 15:36:35 +08:00
tarsin 4f99107ac5 Tweak DF configs 2023-05-28 20:33:44 +08:00
tarsin e4685c7018 Log DoH lookup failure 2023-05-28 19:26:17 +08:00
tarsin 6a42793a59 Fixup! Implement custom DoH 2023-05-28 19:08:02 +08:00
tarsin 4a81df4174 Implement custom DoH 2023-05-28 19:04:28 +08:00
tarsin 512e961820 Add DohPreference 2023-05-28 18:37:50 +08:00
tarsin cbccf7e7fa Add dohUrl settings entry 2023-05-28 17:54:26 +08:00
tarsin f1890c2d3e Cleanup Settings 2023-05-28 17:48:56 +08:00
tarsin 0c30b20a66 Add doh module 2023-05-28 17:43:36 +08:00
tarsin 14b3f13338 Use AndroidAsyncDns 2023-05-28 17:34:00 +08:00
tarsin 0f75f5ec69 Tweak hosts config 2023-05-28 13:57:39 +08:00
tarsin bdbc5438aa Fixup code style 2023-05-28 13:56:51 +08:00
tarsin 525131dbaf Update EhDns.kt 2023-05-28 01:29:08 +08:00
tarsin f8938a46bb Fix naming 2023-05-28 01:18:58 +08:00
tarsin 50ce18d201 Fix codestyle 2023-05-28 01:14:56 +08:00
tarsin bba06321c8 Introduce hostDSL 2023-05-28 01:13:22 +08:00
tarsin 3439b684e1 Drop ip parser 2023-05-28 00:37:30 +08:00
tarsin 0ffba596e5 Cleanup Obsolete db 2023-05-27 23:52:25 +08:00
tarsin c112322daf Drop custom hosts 2023-05-27 23:46:00 +08:00
tarsin a942a487a0 Drop buildMainDB 2023-05-27 22:53:24 +08:00
tarsin 7f02c27a3a Rename EhDatabase to Database 2023-05-27 22:51:37 +08:00
tarsin a69edd5c55 Move CookiesDatabase out of EhDatabase 2023-05-27 22:50:52 +08:00
tarsin f8a0d8b9c5 Move searchDatabase out of EhDatabase 2023-05-27 22:49:12 +08:00
tarsin a7214740ec Avoid mainthread db query 2023-05-27 20:01:26 +08:00
tarsin 17fb0f724c Fix SearchDatabase query 2023-05-27 19:56:38 +08:00
tarsin 5e14dad961 Initial migrate SearchDatabase to ROOM 2023-05-27 19:27:01 +08:00
tarsin dc48363398 Dedup dependencies 2023-05-27 13:02:46 +08:00
tarsin df4a8ec15e Cleanup codestyle 2023-05-27 12:53:03 +08:00
tarsin 810e3ff5fe Use suggestion chip 2023-05-27 12:49:54 +08:00
tarsin aef065a057 Allow open license dialog when badge clicked 2023-05-27 12:41:36 +08:00
tarsin 1824786a80 Open website by default when library item clicked 2023-05-27 01:26:34 +08:00
tarsin c8b7fed168 Drop autogenerated json 2023-05-27 01:19:40 +08:00
tarsin 4118390283 Use tertiaryContainer for badge container 2023-05-27 01:09:59 +08:00
tarsin 13a9fd36d3 Sort libraries by name 2023-05-27 01:03:11 +08:00
tarsin 9ad933934f Add LicenseFragment 2023-05-27 00:55:03 +08:00
tarsin 92240c6c2c Improve MergeInterceptor, only merge normal preview requests 2023-05-26 18:43:40 +08:00
tarsin 689a78d2ee Improve MergeInterceptor 2023-05-26 18:01:45 +08:00
tarsin 7ed0b93faa Cleanup code 2023-05-26 17:37:55 +08:00
tarsin 3a004c41f8 Refactor db export with resourceDSL 2023-05-26 17:15:11 +08:00
tarsin 90ed83efd5 Refactor db import with resourceDSL 2023-05-26 16:58:18 +08:00
tarsin 3a2ccf86fd Fix database import 2023-05-26 16:36:39 +08:00
tarsin 4e6f906903 Convert ThumbKeyMigration to AutoMigrationSpec 2023-05-26 16:27:22 +08:00
tarsin c73ca48e35 Add db v4 schemas 2023-05-26 16:12:55 +08:00
tarsin e1d7b3132d Drop BookmarksDao, upgrade db 2023-05-26 16:02:32 +08:00
tarsin 1f4f99079b Sync with 1.8.8.3 VC 2023-05-26 15:48:03 +08:00
tarsin bf495336d9 Fixup aa1e2102fc 2023-05-26 15:44:55 +08:00
tarsin cc636bcae0 Nonnullize and Immutablize DownloadDirname, upgrade db 2023-05-26 15:44:00 +08:00
tarsin cf185a6148 Export dao schemas 2023-05-26 15:29:16 +08:00
tarsin f92ce637bf Fixup unsafe Composable behaviour 2023-05-26 15:22:50 +08:00
tarsin 2a34d6eb8b Workaround aspect is NaN, Resolve #1116 2023-05-26 15:13:45 +08:00
tarsin a8e3f67371 Fix wrong thumb crop behaviour 2023-05-26 14:59:03 +08:00
tarsin 4a7d4dedd0 Sync with 1.8.8.2 VC 2023-05-26 14:36:55 +08:00
tarsin 7d3178015b Fixup! Resolve #1113 2023-05-26 14:34:58 +08:00
tarsin 54079990d0 Sync with 1.8.8.1 versionCode 2023-05-26 13:48:41 +08:00
tarsin a379391862 Resolve #1113 2023-05-26 13:43:07 +08:00
tarsin dfb2fbf88b Hello, K2! 2023-05-25 16:23:05 +08:00
tarsin 68f201980a Upgrade Kotlin 1.9.0-Beta 2023-05-25 16:14:35 +08:00
tarsin 9a079083e9 Prepare next development iteration 2023-05-25 15:39:16 +08:00
renovate[bot] 1ee1b122fe Update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-SNAPSHOT 2023-05-25 15:37:48 +08:00
541 changed files with 23804 additions and 26838 deletions

No files matched your search

+12 -1
View File
@@ -25,7 +25,18 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Ktlint Check
run: ./gradlew ktlintCheck
+13 -2
View File
@@ -1,6 +1,6 @@
name: Pull Request Check
on: [pull_request]
on: [ pull_request ]
jobs:
linux:
@@ -20,7 +20,18 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Ktlint Check
run: ./gradlew ktlintCheck
+4 -23
View File
@@ -23,7 +23,10 @@ jobs:
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
- name: Gradle Build
uses: gradle/gradle-build-action@v2
@@ -42,7 +45,6 @@ jobs:
uses: softprops/action-gh-release@v1
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/EhViewer-universal.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
@@ -50,24 +52,3 @@ jobs:
app/build/outputs/apk/release/EhViewer-x86.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
app/build/outputs/mapping/release/mapping.txt
- name: Collect Apks
run: |
mkdir app/build/outputs/apk/release/fdroid
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: app/build/outputs/apk/release/fdroid
destination-github-username: Ehviewer-Overhauled
destination-repository-name: fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
create-target-branch: true
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
+107 -46
View File
@@ -1,27 +1,33 @@
import java.io.ByteArrayOutputStream
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
val isRelease: Boolean
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
id("com.mikepenz.aboutlibraries.plugin")
id("org.mozilla.rust-android-gradle.rust-android")
id("dev.shreyaspatil.compose-compiler-report-generator")
}
android {
compileSdk = 33
buildToolsVersion = "33.0.2"
ndkVersion = "25.2.9519653"
androidResources.generateLocaleConfig = true
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10404224"
splits {
abi {
isEnable = true
reset()
if (gradle.startParameter.taskNames.any { it.contains("Release") }) {
if (isRelease) {
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
isUniversalApk = true
} else {
@@ -39,26 +45,26 @@ android {
enableV4Signing = true
}
val commitSha by lazy {
val stdout = ByteArrayOutputStream()
exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
standardOutput = stdout
}
stdout.toString().trim()
}
val commitSha = providers.exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
}.standardOutput.asText.get().trim()
val buildTime by lazy {
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
formatter.format(Instant.now())
}
val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
.removeSuffix(".git")
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180042
versionName = "1.8.8.3"
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.3-gms"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -77,6 +83,7 @@ android {
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
}
externalNativeBuild {
@@ -97,6 +104,7 @@ android {
"-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-XXLanguage:+BreakContinueInInlineLambdas",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
@@ -109,8 +117,9 @@ android {
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
)
}
@@ -150,7 +159,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.7"
kotlinCompilerExtensionVersion = "1.5.1"
}
namespace = "com.hippo.ehviewer"
@@ -158,41 +167,43 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.activity:activity-compose:1.8.0-alpha06")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.5.0")
implementation("androidx.collection:collection-ktx:1.3.0-alpha04")
implementation("androidx.browser:browser:1.6.0")
implementation("androidx.collection:collection-ktx:1.3.0-beta01")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.05.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.04.00-beta01"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.07.00-alpha02"))
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.ui:ui-util")
implementation("androidx.core:core-ktx:1.11.0-beta01")
implementation("androidx.core:core-ktx:1.12.0-rc01")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha10")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.5.3"
val nav_version = "2.7.0"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging
implementation("androidx.paging:paging-common-ktx:3.2.0-alpha06")
implementation("androidx.paging:paging-compose:1.0.0-alpha20")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
val paging = "3.2.0"
implementation("androidx.paging:paging-compose:$paging")
implementation("androidx.paging:paging-runtime-ktx:$paging")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.6.0-alpha01"
val room_version = "2.6.0-alpha03"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
@@ -201,25 +212,33 @@ dependencies {
implementation("com.drakeet.drawer:drawer:1.0.3")
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
implementation("com.github.nanihadesuka:LazyColumnScrollbar:1.6.3")
// https://github.com/google/accompanist/releases
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.30.1")
implementation("com.google.android.material:material:1.9.0")
val accompanist_version = "0.31.6-rc"
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
implementation("com.google.android.material:material:1.11.0-alpha02")
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
implementation("com.louiscad.splitties:splitties-systemservices:$splitties")
implementation("com.louiscad.splitties:splitties-preferences:$splitties")
implementation("com.louiscad.splitties:splitties-arch-room:$splitties")
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
implementation("com.squareup.okhttp3:okhttp-android")
implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation("com.squareup.okio:okio-jvm:3.3.0")
implementation("com.squareup.okio:okio-jvm:3.5.0")
implementation("com.mikepenz:aboutlibraries-core:10.8.3")
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
implementation(platform("io.arrow-kt:arrow-stack:1.2.0-RC"))
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
implementation("io.arrow-kt:arrow-fx-coroutines")
// https://coil-kt.github.io/coil/changelog/
@@ -227,17 +246,59 @@ dependencies {
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.1")
implementation("org.jsoup:jsoup:1.16.1")
}
val ktor = "2.3.3"
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
configurations.all {
exclude("dev.rikka.rikkax.appcompat", "appcompat")
exclude("dev.rikka.rikkax.material", "material")
implementation("org.chromium.net:cronet-api:113.5672.61")
implementation("com.google.android.gms:play-services-cronet:18.0.1")
val serialization = "1.5.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jsoup:jsoup:1.16.1")
val chunker = "4.0.0"
debugImplementation("com.github.chuckerteam.chucker:library:$chunker")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:$chunker")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
}
aboutLibraries {
duplicationMode = MERGE
duplicationRule = GROUP
exclusionPatterns = listOf(
"androidx\\..*".toPattern(),
".*annotations".toPattern(),
"com\\.google\\.code\\..*".toPattern(),
"com\\.google\\.guava:listenablefuture".toPattern(),
"com\\.google\\.protobuf:protobuf-javalite".toPattern(),
"org\\.checkerframework:checker-qual".toPattern(),
"org\\.chromium\\.net:cronet-embedded".toPattern(),
"org\\.slf4j:slf4j-api".toPattern(),
)
}
cargo {
module = "src/main/rust"
libname = "ehviewer_rust"
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64")
if (isRelease) profile = "release"
}
tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
dependsOn("cargoBuild")
// fix mergeDebugJniLibFolders UP-TO-DATE
inputs.dir(buildDir.resolve("rustJniLibs/android"))
}
}
+9
View File
@@ -13,6 +13,15 @@
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
-keep class com.hippo.ehviewer.client.parser.Torrent { *; }
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
@@ -0,0 +1,448 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "613604f93bf815adeb87f03e2166e983",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `TIME` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `FAVORITE_SLOT` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '613604f93bf815adeb87f03e2166e983')"
]
}
}
@@ -0,0 +1,469 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "92915e937cb7ccc536d352fda5cf287f",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL DEFAULT 0, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `TIME` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL DEFAULT 0, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `FAVORITE_SLOT` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '92915e937cb7ccc536d352fda5cf287f')"
]
}
}
@@ -0,0 +1,454 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "d40b38b0ba3b1c53b773728cdee328c8",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `FAVORITE_SLOT` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd40b38b0ba3b1c53b773728cdee328c8')"
]
}
}
@@ -0,0 +1,534 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "df15d8f70f45047a738c8911212ea73c",
"entities": [
{
"tableName": "GALLERIES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `FAVORITE_SLOT` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'df15d8f70f45047a738c8911212ea73c')"
]
}
}
@@ -0,0 +1,402 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "54e4f2704cc47c5a6a2f4b96de978cce",
"entities": [
{
"tableName": "GALLERIES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '54e4f2704cc47c5a6a2f4b96de978cce')"
]
}
}
@@ -0,0 +1,412 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "991a3a3ddc4fe8d4dbcb39b975ab73b8",
"entities": [
{
"tableName": "GALLERIES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [
{
"name": "index_DOWNLOAD_LABELS_LABEL",
"unique": true,
"columnNames": [
"LABEL"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
}
],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '991a3a3ddc4fe8d4dbcb39b975ab73b8')"
]
}
}
@@ -0,0 +1,433 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "06e5ff8fd6797d5801e86753fa5831b2",
"entities": [
{
"tableName": "GALLERIES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "favoriteSlot",
"columnName": "FAVORITE_SLOT",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [
{
"name": "index_DOWNLOAD_LABELS_LABEL",
"unique": true,
"columnNames": [
"LABEL"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
}
],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [
{
"name": "index_DOWNLOADS_LABEL",
"unique": false,
"columnNames": [
"LABEL"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
}
],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
},
{
"table": "DOWNLOAD_LABELS",
"onDelete": "SET NULL",
"onUpdate": "CASCADE",
"columns": [
"LABEL"
],
"referencedColumns": [
"LABEL"
]
}
]
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": [
{
"table": "GALLERIES",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "POSITION",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '06e5ff8fd6797d5801e86753fa5831b2')"
]
}
}
@@ -0,0 +1,534 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "f5752deda842dccabded1cdbf1c78384",
"entities": [
{
"tableName": "BOOKMARKS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`PAGE` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "page",
"columnName": "PAGE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumb",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumb",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `LABEL` TEXT, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT, `ENABLE` INTEGER, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumb",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumb",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5752deda842dccabded1cdbf1c78384')"
]
}
}
@@ -0,0 +1,534 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "f5752deda842dccabded1cdbf1c78384",
"entities": [
{
"tableName": "BOOKMARKS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`PAGE` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "page",
"columnName": "PAGE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `LABEL` TEXT, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT, `ENABLE` INTEGER, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f5752deda842dccabded1cdbf1c78384')"
]
}
}
@@ -0,0 +1,534 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "e45f4380ef769c127604cb2a502a942b",
"entities": [
{
"tableName": "BOOKMARKS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`PAGE` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "page",
"columnName": "PAGE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `LABEL` TEXT, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT, `ENABLE` INTEGER, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e45f4380ef769c127604cb2a502a942b')"
]
}
}
@@ -0,0 +1,448 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "9563c2017be4341dc1bada115426df77",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `LABEL` TEXT, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT, `ENABLE` INTEGER, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9563c2017be4341dc1bada115426df77')"
]
}
}
@@ -0,0 +1,448 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "f09bae277bbdab54ecbb0d698c83e734",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `LABEL` TEXT, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f09bae277bbdab54ecbb0d698c83e734')"
]
}
}
@@ -0,0 +1,448 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "64f51a531e13d2b0b971f3d8da0876b0",
"entities": [
{
"tableName": "DOWNLOADS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "state",
"columnName": "STATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "legacy",
"columnName": "LEGACY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_LABELS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `TIME` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "label",
"columnName": "LABEL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DOWNLOAD_DIRNAME",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dirname",
"columnName": "DIRNAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "FILTER",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "TEXT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "ENABLE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HISTORY",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `MODE` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favoriteSlotBackingField",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LOCAL_FAVORITES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`TIME` INTEGER NOT NULL, `GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, PRIMARY KEY(`GID`))",
"fields": [
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "TOKEN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbKey",
"columnName": "THUMB",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "posted",
"columnName": "POSTED",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "UPLOADER",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "RATING",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "simpleLanguage",
"columnName": "SIMPLE_LANGUAGE",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "QUICK_SEARCH",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "MODE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "category",
"columnName": "CATEGORY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "keyword",
"columnName": "KEYWORD",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "advanceSearch",
"columnName": "ADVANCE_SEARCH",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minRating",
"columnName": "MIN_RATING",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageFrom",
"columnName": "PAGE_FROM",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pageTo",
"columnName": "PAGE_TO",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "TIME",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '64f51a531e13d2b0b971f3d8da0876b0')"
]
}
}
@@ -0,0 +1,46 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "69230710633494b4b331e10dae436fb7",
"entities": [
{
"tableName": "suggestions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `date` LONG, `query` TEXT, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "LONG",
"notNull": false
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '69230710633494b4b331e10dae436fb7')"
]
}
}
@@ -0,0 +1,46 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "bf4a5b5f3cb9bc7383488bae14f5d4bd",
"entities": [
{
"tableName": "suggestions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `query` TEXT NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf4a5b5f3cb9bc7383488bae14f5d4bd')"
]
}
}
+8 -13
View File
@@ -12,7 +12,8 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
@@ -22,21 +23,21 @@
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locale_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<activity
android:name=".ui.login.LoginActivity"
android:name=".ui.ConfigureActivity"
android:exported="false"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"/>
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
@@ -69,8 +70,6 @@
<data android:scheme="https" />
<data android:host="exhentai.org" />
<data android:host="e-hentai.org" />
<data android:host="g.e-hentai.org" />
<data android:host="lofi.e-hentai.org" />
</intent-filter>
<intent-filter android:label="@string/keyword_search">
<action android:name="android.intent.action.SEND" />
@@ -125,22 +124,18 @@
<data android:mimeType="application/x-xz-compressed-tar" />
</intent-filter>
</activity>
<activity
android:name=".ui.SettingsActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
android:label="@string/settings" />
<activity
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name=".ui.WebViewActivity" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@@ -1,6 +1,6 @@
From fc9fb0ea245259e346ab46c334f03b557fe63b6b Mon Sep 17 00:00:00 2001
From 2bda36800a5424c44378bbc0cc8bb5d26a0f34fa Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Tue, 22 Nov 2022 12:37:12 +0800
Date: Sat, 22 Jul 2023 08:30:20 +0800
Subject: [PATCH] Don't build other binarys
---
@@ -8,19 +8,19 @@ Subject: [PATCH] Don't build other binarys
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 01c73cd..b461e68 100644
index 55cd358..52b57aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -592,7 +592,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
@@ -962,7 +962,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG)
-if(HAVE_GETOPT_LONG AND HAVE_DECODERS)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -630,7 +630,7 @@ endif()
@@ -1008,7 +1008,7 @@ endif()
# xz
#############################################################################
@@ -30,5 +30,5 @@ index 01c73cd..b461e68 100644
src/common/mythread.h
src/common/sysdefs.h
--
2.39.2
2.41.0
@@ -1,38 +0,0 @@
From 2a67861e6f06794f3671c4988bac12330d8422e3 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Fri, 27 May 2022 23:21:20 +0800
Subject: [PATCH 1/2] Don't build shared lib
---
libarchive/CMakeLists.txt | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt
index ff7ade00..00c2e170 100644
--- a/libarchive/CMakeLists.txt
+++ b/libarchive/CMakeLists.txt
@@ -242,12 +242,6 @@ ELSEIF(ARCHIVE_ACL_SUNOS)
LIST(APPEND libarchive_SOURCES archive_disk_acl_sunos.c)
ENDIF()
-# Libarchive is a shared library
-ADD_LIBRARY(archive SHARED ${libarchive_SOURCES} ${include_HEADERS})
-TARGET_INCLUDE_DIRECTORIES(archive PUBLIC .)
-TARGET_LINK_LIBRARIES(archive ${ADDITIONAL_LIBS})
-SET_TARGET_PROPERTIES(archive PROPERTIES SOVERSION ${SOVERSION})
-
# archive_static is a static library
ADD_LIBRARY(archive_static STATIC ${libarchive_SOURCES} ${include_HEADERS})
TARGET_LINK_LIBRARIES(archive_static ${ADDITIONAL_LIBS})
@@ -260,7 +254,7 @@ ENDIF(NOT WIN32 OR CYGWIN)
IF(ENABLE_INSTALL)
# How to install the libraries
- INSTALL(TARGETS archive archive_static
+ INSTALL(TARGETS archive_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
--
2.39.2
+29
View File
@@ -0,0 +1,29 @@
From 57da49f9b0ed8538573f6cfe289f835ee0078eec Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 17:50:08 +0800
Subject: [PATCH] Hide symbols
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 646484b7..67a71272 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2060,10 +2060,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
-IF (HAVE_VISIBILITY_ATTR)
+IF (0)
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
-ENDIF(HAVE_VISIBILITY_ATTR)
+ENDIF(0)
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
# Generate "config.h" from "build/cmake/config.h.in"
--
2.41.0
+12 -4
View File
@@ -3,7 +3,7 @@ project(ehviewer C ASM)
include(FetchContent)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
@@ -20,9 +20,11 @@ endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
set(FETCHCONTENT_BASE_DIR ${deps})
set(BUILD_TESTING 0)
FetchContent_Declare(liblzma
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.1
GIT_TAG v5.4.3
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || cd .
)
@@ -50,13 +52,19 @@ SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
SET(HAVE_LZMADEC_H 1)
SET(HAVE_LIBLZMADEC 1)
option(ENABLE_TAR OFF)
option(ENABLE_CPIO OFF)
option(ENABLE_CAT OFF)
option(ENABLE_UNZIP OFF)
option(ENABLE_TEST OFF)
# Configure libarchive link's static lib
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.6.2
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-shared-lib.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
GIT_TAG v3.7.0
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
)
FetchContent_MakeAvailable(libarchive)
@@ -1,5 +1,6 @@
package androidx.navigation.ui
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
@@ -8,8 +9,8 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
@@ -20,10 +21,10 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(com.hippo.ehviewer.R.anim.scene_open_enter)
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
builder.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
}
val options = builder.build()
return try {
@@ -22,7 +22,7 @@ import androidx.annotation.IntDef
import com.davemorrissey.labs.subscaleview.decoder.Decoder
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -31,7 +31,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.ArrayList
import java.util.Locale
import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
@@ -442,8 +441,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
detector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (panEnabled && isReady && vTranslate != null && e1 != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
@@ -463,7 +462,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (!ReaderActivity.readerPreferences.doubleTapToZoom().get()) return false
if (!ReaderPreferences.doubleTapToZoom().get()) return false
if (isZoomEnabled && isReady && vTranslate != null) {
// Hacky solution for #15 - after a double tap the GestureDetector gets in a state
// where the next fling is ignored, so here we replace it with a new one.
@@ -1713,7 +1712,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param sTarget target object for result. The same instance is also returned.
* @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param.
*/
@JvmOverloads
fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
if (vTranslate == null) {
return null
@@ -1765,7 +1763,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
* @param vTarget target object for result. The same instance is also returned.
* @return view coordinates. This is the same instance passed to the vTarget param.
*/
@JvmOverloads
fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
if (vTranslate == null) {
return null
@@ -5,8 +5,6 @@ import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import java.lang.Exception
import kotlin.Throws
/**
* Interface for image decoding classes.
@@ -21,7 +19,6 @@ interface Decoder {
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
fun init(context: Context, provider: InputProvider): Point
/**
@@ -7,10 +7,7 @@ import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
bitmapConfig: Bitmap.Config? = null,
) : Decoder {
class ImageDecoder(private val cropBorders: Boolean, bitmapConfig: Bitmap.Config? = null) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
}
@@ -24,7 +21,6 @@ class ImageDecoder @JvmOverloads constructor(
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
override fun init(context: Context, provider: InputProvider): Point {
TODO()
}
@@ -1,177 +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;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.ehviewer.util.ReadableTime;
import com.hippo.ehviewer.yorozuya.FileUtils;
import com.hippo.ehviewer.yorozuya.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class AppConfig {
public static final String APP_DIRNAME = "EhViewer";
private static final String DOWNLOAD = "download";
private static final String TEMP = "temp";
private static final String PARSE_ERROR = "parse_error";
private static final String CRASH = "crash";
@SuppressLint("StaticFieldLeak")
private static Context sContext;
public static void initialize(Context context) {
sContext = context.getApplicationContext();
}
@Nullable
public static File getExternalAppDir() {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File dir = sContext.getExternalFilesDir(null);
return FileUtils.ensureDirectory(dir) ? dir : null;
}
return null;
}
/**
* mkdirs and get
*/
@Nullable
public static File getDirInExternalAppDir(String filename) {
File appFolder = getExternalAppDir();
if (appFolder != null) {
File dir = new File(appFolder, filename);
return FileUtils.ensureDirectory(dir) ? dir : null;
}
return null;
}
@Nullable
public static File getFileInExternalAppDir(String filename) {
File appFolder = getExternalAppDir();
if (appFolder != null) {
File file = new File(appFolder, filename);
return FileUtils.ensureFile(file) ? file : null;
}
return null;
}
@Nullable
public static File getDefaultDownloadDir() {
return getDirInExternalAppDir(DOWNLOAD);
}
@Nullable
public static File getExternalTempDir() {
File dir = sContext.getExternalCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
return file;
} else {
return null;
}
}
@Nullable
public static File getExternalCopyTempDir() {
File dir = sContext.getExternalCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, "copy"))) {
return file;
} else {
return null;
}
}
@Nullable
public static File getExternalParseErrorDir() {
return getDirInExternalAppDir(PARSE_ERROR);
}
@Nullable
public static File getExternalCrashDir() {
return getDirInExternalAppDir(CRASH);
}
@Nullable
public static File getTempDir() {
File dir = sContext.getCacheDir();
File file;
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
return file;
} else {
return null;
}
}
@Nullable
public static File createTempFile() {
return FileUtils.createTempFile(getTempDir(), null);
}
public static void saveParseErrorBody(@NonNull ParseException e) {
File dir = getExternalParseErrorDir();
if (null == dir) {
return;
}
File file = new File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt");
OutputStream os = null;
try {
os = new FileOutputStream(file);
String message = e.getMessage();
String body = e.getBody();
if (null != message) {
os.write(message.getBytes(StandardCharsets.UTF_8));
os.write('\n');
}
os.write(body.getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (IOException e1) {
// Ignore
} finally {
IOUtils.closeQuietly(os);
}
}
@Nullable
public static File getFilesDir(String name) {
File dir = sContext.getFilesDir();
if (dir == null) {
return null;
}
dir = new File(dir, name);
if (dir.isDirectory() || dir.mkdirs()) {
return dir;
} else {
return null;
}
}
}
@@ -20,45 +20,53 @@ import android.app.Application
import android.content.ComponentCallbacks2
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import androidx.lifecycle.coroutineScope
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.diskCache
import com.hippo.ehviewer.coil.installCronetHttpUriFetcher
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.dao.SearchDatabase
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.cache
import com.hippo.ehviewer.ktbuilder.chunker
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.ktbuilder.httpClient
import com.hippo.ehviewer.ktbuilder.imageLoader
import com.hippo.ehviewer.legacy.cleanObsoleteCache
import com.hippo.ehviewer.legacy.migrateCookies
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.Crash
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.yorozuya.FileUtils
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isCronetSupported
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import okhttp3.Cache
import okhttp3.OkHttpClient
import okio.FileSystem
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.launch
import moe.tarsin.kt.unreachable
import okhttp3.AsyncDns
import okhttp3.android.AndroidAsyncDns
import okio.Path.Companion.toOkioPath
import java.net.Proxy
import java.security.KeyStore
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import splitties.arch.room.roomDb
import splitties.init.appCtx
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
@OptIn(DelicateCoroutinesApi::class)
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope
class EhApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
application = this
lifecycle.addObserver(lockObserver)
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
@@ -69,18 +77,26 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
handler?.uncaughtException(t, e)
}
super<Application>.onCreate()
super.onCreate()
CronetProviderInstaller.installProvider(appCtx)
System.loadLibrary("ehviewer")
Settings.initialize()
System.loadLibrary("ehviewer_rust")
ReadableTime.initialize(this)
AppConfig.initialize(this)
AppCompatDelegate.setDefaultNightMode(Settings.theme)
launchIO {
lifecycleScope.launchIO {
val theme = Settings.theme
withUIContext {
AppCompatDelegate.setDefaultNightMode(theme)
}
}
lifecycleScope.launchIO {
launchIO {
migrateCookies()
}
launchIO {
EhTagDatabase.update()
}
launchIO {
ehDatabase
EhDB
}
launchIO {
DownloadManager.isIdle
@@ -111,11 +127,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
private fun clearTempDir() {
var dir = AppConfig.getTempDir()
var dir = AppConfig.tempDir
if (null != dir) {
FileUtils.deleteContent(dir)
}
dir = AppConfig.getExternalTempDir()
dir = AppConfig.externalTempDir
if (null != dir) {
FileUtils.deleteContent(dir)
}
@@ -128,85 +144,67 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
override fun onPause(owner: LifecycleOwner) {
if (!locked) {
locked_last_leave_time = System.currentTimeMillis() / 1000
}
locked = true
}
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(nonCacheOkHttpClient)
components {
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
override fun newImageLoader() = imageLoader {
components {
if (isCronetSupported) {
callFactory { unreachable() }
installCronetHttpUriFetcher()
} else {
callFactory(baseOkHttpClient)
}
diskCache(imageCache)
crossfade(300)
error(R.drawable.image_failed)
if (BuildConfig.DEBUG) logger(DebugLogger())
}.build()
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
}
diskCache(imageCache)
crossfade(300)
error(R.drawable.image_failed)
if (BuildConfig.DEBUG) logger(DebugLogger())
respectCacheHeaders(false)
}
companion object {
var locked = true
var locked_last_leave_time: Long = 0
lateinit var application: EhApplication
private set
val ehProxySelector by lazy { EhProxySelector() }
val nonCacheOkHttpClient by lazy {
OkHttpClient.Builder().apply {
val baseOkHttpClient by lazy {
httpClient {
cookieJar(EhCookieStore)
dns(EhDns)
proxySelector(ehProxySelector)
if (Settings.dF) {
val factory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
factory.init(null as KeyStore?)
val manager = factory.trustManagers!!
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
sslSocketFactory(EhSSLSocketFactory, trustManager)
proxy(Proxy.NO_PROXY)
if (isAtLeastQ) {
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
}
}.build()
chunker { alwaysReadResponseBody(false) }
addInterceptor(CloudflareInterceptor(appCtx))
}
}
val noRedirectOkHttpClient by lazy {
httpClient(baseOkHttpClient) {
followRedirects(false)
}
}
// Never use this okhttp client to download large blobs!!!
val okHttpClient by lazy {
nonCacheOkHttpClient.newBuilder().cache(
Cache(
application.cacheDir.toOkioPath() / "http_cache",
httpClient(baseOkHttpClient) {
cache(
appCtx.cacheDir.toOkioPath() / "http_cache",
20L * 1024L * 1024L,
FileSystem.SYSTEM,
),
).build()
)
}
}
val galleryDetailCache by lazy {
LruCache<Long, GalleryDetail>(25).also {
favouriteStatusRouter.addListener { gid, slot ->
it[gid]?.favoriteSlot = slot
lifecycleScope.launch {
FavouriteStatusRouter.globalFlow.collect { (gid, slot) -> it[gid]?.favoriteSlot = slot }
}
}
}
val hosts by lazy { Hosts(application, "hosts.db") }
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
val ehDatabase by lazy { buildMainDB(application) }
val imageCache by lazy {
diskCache {
directory(application.cacheDir.toOkioPath() / "image_cache")
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.coerceIn(320, 5120).toLong() * 1024 * 1024)
}
}
val searchDatabase by lazy { roomDb<SearchDatabase>("search_database.db") }
}
}
+153 -294
View File
@@ -19,15 +19,12 @@ import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import androidx.paging.PagingSource
import androidx.room.Room.databaseBuilder
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.BasicDao
import arrow.fx.coroutines.release
import arrow.fx.coroutines.resource
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EHDB_MIGRATION_4_5
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
@@ -35,90 +32,76 @@ import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
import kotlinx.coroutines.flow.Flow
import splitties.arch.room.roomDb
object EhDB {
private const val CUR_DB_VER = 4
private val db = ehDatabase
private val db = roomDb<EhDatabase>("eh.db")
// Fix state
@get:Synchronized
val allDownloadInfo: List<DownloadInfo>
get() = db.downloadsDao().list().onEach {
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
it.state = DownloadInfo.STATE_NONE
}
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
db.galleryDao().upsert(galleryInfo)
}
private suspend fun deleteGalleryInfo(galleryInfo: BaseGalleryInfo) {
runCatching { db.galleryDao().delete(galleryInfo) }
}
suspend fun getAllDownloadInfo() = db.downloadsDao().joinList().onEach {
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
it.state = DownloadInfo.STATE_NONE
}
}
@Synchronized
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
suspend fun updateDownloadInfo(downloadInfo: List<DownloadInfo>) {
val dao = db.downloadsDao()
dao.update(downloadInfos)
dao.update(downloadInfo.map(DownloadInfo::downloadInfo))
}
@Synchronized
fun putDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().run {
if (load(downloadInfo.gid) != null) {
update(downloadInfo)
} else {
insert(downloadInfo)
}
suspend fun putDownloadInfo(downloadInfo: DownloadInfo) {
putGalleryInfo(downloadInfo.galleryInfo)
db.downloadsDao().upsert(downloadInfo.downloadInfo)
}
suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) {
val dao = db.downloadsDao()
dao.delete(downloadInfo.downloadInfo)
dao.fill(downloadInfo.position)
deleteGalleryInfo(downloadInfo.galleryInfo)
}
suspend fun removeDownloadInfo(downloadInfo: List<DownloadInfo>) {
val dao = db.downloadsDao()
downloadInfo.forEach {
dao.delete(it.downloadInfo)
deleteGalleryInfo(it.galleryInfo)
}
dao.fill(downloadInfo.first().position, downloadInfo.size)
}
@Synchronized
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().delete(downloadInfo)
}
@Synchronized
fun getDownloadDirname(gid: Long): String? {
suspend fun getDownloadDirname(gid: Long): String? {
val dao = db.downloadDirnameDao()
val raw = dao.load(gid)
return raw?.dirname
}
@Synchronized
fun putDownloadDirname(gid: Long, dirname: String?) {
suspend fun putDownloadDirname(gid: Long, dirname: String) {
val dao = db.downloadDirnameDao()
var raw = dao.load(gid)
if (raw != null) {
raw.dirname = dirname
dao.update(raw)
} else {
raw = DownloadDirname(gid, dirname)
dao.insert(raw)
}
dao.upsert(DownloadDirname(gid, dirname))
}
@Synchronized
fun removeDownloadDirname(gid: Long) {
suspend fun removeDownloadDirname(gid: Long) {
val dao = db.downloadDirnameDao()
dao.deleteByKey(gid)
}
@Synchronized
fun clearDownloadDirname() {
private suspend fun importDownloadDirname(downloadDirnameList: List<DownloadDirname>) {
val dao = db.downloadDirnameDao()
dao.deleteAll()
dao.insertOrIgnore(downloadDirnameList)
}
@get:Synchronized
val allDownloadLabelList: List<DownloadLabel>
get() = db.downloadLabelDao().list()
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
@Synchronized
fun addDownloadLabel(label: String): DownloadLabel {
val dao = db.downloadLabelDao()
val raw = DownloadLabel()
raw.label = label
raw.time = System.currentTimeMillis()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
// Reset id
raw.id = null
val dao = db.downloadLabelDao()
@@ -126,221 +109,122 @@ object EhDB {
return raw
}
@Synchronized
fun updateDownloadLabel(raw: DownloadLabel?) {
suspend fun updateDownloadLabel(raw: DownloadLabel) {
val dao = db.downloadLabelDao()
dao.update(raw!!)
dao.update(raw)
}
@Synchronized
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
suspend fun updateDownloadLabel(downloadLabels: List<DownloadLabel>) {
val dao = db.downloadLabelDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
dao.update(downloadLabels)
}
@Synchronized
fun removeDownloadLabel(raw: DownloadLabel?) {
suspend fun removeDownloadLabel(raw: DownloadLabel) {
val dao = db.downloadLabelDao()
dao.delete(raw!!)
dao.delete(raw)
dao.fill(raw.position)
}
@get:Synchronized
val allLocalFavorites: List<GalleryInfo>
get() {
val dao = db.localFavoritesDao()
val list = dao.list()
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun searchLocalFavorites(query: String): List<GalleryInfo> {
val dao = db.localFavoritesDao()
val list = dao.list("%$query%")
return ArrayList<GalleryInfo>(list)
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
}
@Synchronized
fun removeLocalFavorites(gid: Long) {
db.localFavoritesDao().deleteByKey(gid)
}
@Synchronized
fun removeLocalFavorites(gidArray: LongArray) {
val dao = db.localFavoritesDao()
for (gid in gidArray) {
dao.deleteByKey(gid)
suspend fun removeLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
galleryInfoList.forEach {
removeLocalFavorites(it)
}
}
@JvmStatic
@Synchronized
fun containLocalFavorites(gid: Long): Boolean {
suspend fun containLocalFavorites(gid: Long): Boolean {
val dao = db.localFavoritesDao()
return dao.contains(gid)
}
@Synchronized
fun putLocalFavorites(galleryInfo: GalleryInfo) {
val dao = db.localFavoritesDao()
if (null == dao.load(galleryInfo.gid)) {
val info: LocalFavoriteInfo
if (galleryInfo is LocalFavoriteInfo) {
info = galleryInfo
} else {
info = LocalFavoriteInfo(galleryInfo)
info.time = System.currentTimeMillis()
}
dao.insert(info)
suspend fun putLocalFavorites(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
}
suspend fun putLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
galleryInfoList.forEach {
putLocalFavorites(it)
}
}
@Synchronized
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
for (gi in galleryInfoList) {
putLocalFavorites(gi)
}
private suspend fun importLocalFavorites(localFavorites: List<LocalFavoriteInfo>) {
db.localFavoritesDao().insertOrIgnore(localFavorites)
}
@get:Synchronized
val allQuickSearch: List<QuickSearch>
get() {
val dao = db.quickSearchDao()
return dao.list()
}
suspend fun getAllQuickSearch() = db.quickSearchDao().list()
@Synchronized
fun insertQuickSearch(quickSearch: QuickSearch) {
suspend fun insertQuickSearch(quickSearch: QuickSearch) {
val dao = db.quickSearchDao()
quickSearch.id = null
quickSearch.time = System.currentTimeMillis()
quickSearch.id = dao.insert(quickSearch)
}
@Synchronized
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
private suspend fun importQuickSearch(quickSearchList: List<QuickSearch>) {
val dao = db.quickSearchDao()
for (quickSearch in quickSearchList) {
dao.insert(quickSearch!!)
}
dao.insert(quickSearchList)
}
@Synchronized
fun deleteQuickSearch(quickSearch: QuickSearch?) {
suspend fun deleteQuickSearch(quickSearch: QuickSearch) {
val dao = db.quickSearchDao()
if (quickSearch != null) {
dao.delete(quickSearch)
}
dao.delete(quickSearch)
dao.fill(quickSearch.position)
}
@Synchronized
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
suspend fun updateQuickSearch(quickSearchList: List<QuickSearch>) {
val dao = db.quickSearchDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
dao.update(quickSearchList)
}
@get:Synchronized
val historyLazyList: PagingSource<Int, HistoryInfo>
get() = db.historyDao().listLazy()
val historyLazyList
get() = db.historyDao().joinListLazy()
@Synchronized
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
val dao = db.historyDao()
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
galleryInfo
} else {
HistoryInfo(galleryInfo!!)
}
info.time = System.currentTimeMillis()
if (null != dao.load(info.gid)) {
dao.update(info)
} else {
dao.insert(info)
val localFavLazyList
get() = db.localFavoritesDao().joinListLazy()
val localFavCount: Flow<Int>
get() = db.localFavoritesDao().count()
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("%$keyword%")
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
}
suspend fun modifyHistoryInfoFavslotNonRefresh(gid: Long, slot: Int) {
val dao = db.galleryDao()
dao.load(gid)?.let {
it.favoriteSlot = slot
dao.update(it)
}
}
@Synchronized
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
private suspend fun importHistoryInfo(historyInfoList: List<HistoryInfo>) {
val dao = db.historyDao()
val i = dao.load(info.gid)
if (null != i) {
val historyInfo: HistoryInfo
if (info is HistoryInfo) {
historyInfo = info
} else {
historyInfo = HistoryInfo(info)
historyInfo.time = i.time
}
dao.update(historyInfo)
}
dao.insertOrIgnore(historyInfoList)
}
@Synchronized
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
suspend fun deleteHistoryInfo(galleryInfo: BaseGalleryInfo) {
val dao = db.historyDao()
for (info in historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info)
}
}
dao.deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
}
@Synchronized
fun deleteHistoryInfo(info: HistoryInfo?) {
val dao = db.historyDao()
dao.delete(info!!)
}
@Synchronized
fun clearHistoryInfo() {
suspend fun clearHistoryInfo() {
val dao = db.historyDao()
val historyList = dao.list()
dao.deleteAll()
historyList.forEach { runCatching { db.galleryDao().deleteByKey(it.gid) } }
}
@get:Synchronized
val allFilter: List<Filter>
get() = db.filterDao().list()
suspend fun getAllFilter() = db.filterDao().list()
@Synchronized
fun addFilter(filter: Filter): Boolean {
val existFilter: Filter? = try {
db.filterDao().load(filter.text!!, filter.mode)
} catch (e: Exception) {
null
}
suspend fun addFilter(filter: Filter): Boolean {
val existFilter = runCatching { db.filterDao().load(filter.text, filter.mode.field) }.getOrNull()
return if (existFilter == null) {
filter.id = null
filter.id = db.filterDao().insert(filter)
@@ -350,35 +234,31 @@ object EhDB {
}
}
@Synchronized
fun deleteFilter(filter: Filter) {
suspend fun deleteFilter(filter: Filter) {
db.filterDao().delete(filter)
}
@Synchronized
fun triggerFilter(filter: Filter) {
filter.enable = filter.enable?.not() ?: false
suspend fun updateFilter(filter: Filter) {
db.filterDao().update(filter)
}
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
val list = from.list()
for (item in list) to.insert(item)
}
@Synchronized
fun exportDB(context: Context, uri: Uri): Boolean {
suspend fun exportDB(context: Context, uri: Uri) {
val ehExportName = "eh.export.db"
runCatching {
resource {
context.deleteDatabase(ehExportName)
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
copyDao(db.downloadsDao(), newDb.downloadsDao())
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
copyDao(db.historyDao(), newDb.historyDao())
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
copyDao(db.filterDao(), newDb.filterDao())
roomDb<EhDatabase>(ehExportName)
} release {
it.close()
context.deleteDatabase(ehExportName)
} use { newDb ->
db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) }
db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) }
db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) }
db.downloadsDao().list().let { newDb.downloadsDao().insert(it) }
db.historyDao().list().let { newDb.historyDao().insertOrIgnore(it) }
db.quickSearchDao().list().let { newDb.quickSearchDao().insert(it) }
db.localFavoritesDao().list().let { newDb.localFavoritesDao().insertOrIgnore(it) }
db.filterDao().list().let { newDb.filterDao().insert(it) }
newDb.close()
val dbFile = context.getDatabasePath(ehExportName)
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
@@ -386,68 +266,52 @@ object EhDB {
fromFd sendTo toFd
}
}
return true
}.onFailure {
it.printStackTrace()
}
return false
}
/**
* @return error string, null for no error
*/
@Synchronized
fun importDB(context: Context, uri: Uri): String? {
runCatching {
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
.createFromInputStream { context.contentResolver.openInputStream(uri) }
.addMigrations(EHDB_MIGRATION_4_5)
.build()
// Download label
val manager = DownloadManager
suspend fun importDB(context: Context, uri: Uri) {
val tempDBName = "tmp.db"
resource {
context.deleteDatabase(tempDBName)
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
} release {
it.close()
context.deleteDatabase(tempDBName)
} use { oldDB ->
runCatching {
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
}
runCatching {
val downloadLabelList = oldDB.downloadLabelDao().list()
manager.addDownloadLabel(downloadLabelList)
DownloadManager.addDownloadLabel(downloadLabelList)
}
// Downloads
runCatching {
val downloadInfoList = oldDB.downloadsDao().list()
manager.addDownload(downloadInfoList, false)
}
// Download dirname
runCatching {
oldDB.downloadDirnameDao().list().forEach {
putDownloadDirname(it.gid, it.dirname)
oldDB.downloadDirnameDao().list().let {
importDownloadDirname(it)
}
}
// History
runCatching {
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
DownloadManager.addDownload(downloadInfoList, false)
}
runCatching {
val historyInfoList = oldDB.historyDao().list()
putHistoryInfo(historyInfoList)
importHistoryInfo(historyInfoList)
}
// QuickSearch
runCatching {
val quickSearchList = oldDB.quickSearchDao().list()
val currentQuickSearchList = db.quickSearchDao().list()
val importList = quickSearchList.mapNotNull { newQS ->
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
}
val offset = currentQuickSearchList.size
val importList = quickSearchList.filter { newQS ->
currentQuickSearchList.none { it.name == newQS.name }
}.onEachIndexed { index, q -> q.position = index + offset }
importQuickSearch(importList)
}
// LocalFavorites
runCatching {
oldDB.localFavoritesDao().list().forEach {
putLocalFavorites(it)
oldDB.localFavoritesDao().list().let {
importLocalFavorites(it)
}
}
// Filter
runCatching {
val filterList = oldDB.filterDao().list()
val currentFilterList = db.filterDao().list()
@@ -455,11 +319,6 @@ object EhDB {
if (it !in currentFilterList) addFilter(it)
}
}
oldDB.close()
}.onFailure {
it.printStackTrace()
return context.getString(R.string.cant_read_the_file)
}
return null
}
}
@@ -1,92 +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
import android.text.TextUtils
import com.hippo.ehviewer.Settings.proxyIp
import com.hippo.ehviewer.Settings.proxyPort
import com.hippo.ehviewer.Settings.proxyType
import com.hippo.ehviewer.network.InetValidator.isValidInetPort
import com.hippo.ehviewer.util.ExceptionUtils
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
class EhProxySelector internal constructor() : ProxySelector() {
private var delegation: ProxySelector? = null
private var alternative: ProxySelector?
init {
alternative = getDefault() ?: NullProxySelector()
updateProxy()
}
fun updateProxy() {
delegation = when (proxyType) {
TYPE_DIRECT -> NullProxySelector()
TYPE_SYSTEM -> alternative
TYPE_HTTP, TYPE_SOCKS -> null
else -> alternative
}
}
override fun select(uri: URI): List<Proxy> {
val type = proxyType
if (type == TYPE_HTTP || type == TYPE_SOCKS) {
runCatching {
val ip = proxyIp
val port = proxyPort
if (!TextUtils.isEmpty(ip) && isValidInetPort(port)) {
val inetAddress = InetAddress.getByName(ip)
val socketAddress = InetSocketAddress(inetAddress, port)
return listOf(
Proxy(
if (type == TYPE_HTTP) Proxy.Type.HTTP else Proxy.Type.SOCKS,
socketAddress,
),
)
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
}
}
return delegation?.select(uri) ?: alternative!!.select(uri)
}
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {
delegation?.select(uri)
}
private class NullProxySelector : ProxySelector() {
override fun select(uri: URI): List<Proxy> {
return listOf(Proxy.NO_PROXY)
}
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {}
}
companion object {
const val TYPE_DIRECT = 0
const val TYPE_SYSTEM = 1
const val TYPE_HTTP = 2
const val TYPE_SOCKS = 3
}
}
@@ -1,74 +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;
import android.annotation.SuppressLint;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.yorozuya.IntIdGenerator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class FavouriteStatusRouter {
private static final String KEY_DATA_MAP_NEXT_ID = "data_map_next_id";
private final IntIdGenerator idGenerator = new IntIdGenerator(Settings.getInt(KEY_DATA_MAP_NEXT_ID, 0));
@SuppressLint("UseSparseArrays")
private final HashMap<Integer, Map<Long, GalleryInfo>> maps = new HashMap<>();
private final List<Listener> listeners = new ArrayList<>();
public int saveDataMap(Map<Long, GalleryInfo> map) {
int id = idGenerator.nextId();
maps.put(id, map);
Settings.putInt(KEY_DATA_MAP_NEXT_ID, idGenerator.nextId());
return id;
}
public Map<Long, GalleryInfo> restoreDataMap(int id) {
return maps.remove(id);
}
public void modifyFavourites(long gid, int slot) {
for (Map<Long, GalleryInfo> map : maps.values()) {
GalleryInfo info = map.get(gid);
if (info != null) {
info.setFavoriteSlot(slot);
}
}
for (Listener listener : listeners) {
listener.onModifyFavourites(gid, slot);
}
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public interface Listener {
void onModifyFavourites(long gid, int slot);
}
}
@@ -1,343 +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;
/*
* Created by Hippo on 2018/3/21.
*/
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Pair;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.database.MSQLiteBuilder;
import com.hippo.ehviewer.util.SqlUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
public class Hosts {
private static final int VERSION_1 = 1;
private static final String TABLE_HOSTS = "HOSTS";
private static final String COLUMN_HOST = "HOST";
private static final String COLUMN_IP = "IP";
private static final int DB_VERSION = VERSION_1;
private final SQLiteDatabase db;
public Hosts(Context context, String name) {
SQLiteOpenHelper helper = new MSQLiteBuilder()
.version(VERSION_1)
.createTable(TABLE_HOSTS)
.insertColumn(TABLE_HOSTS, COLUMN_HOST, String.class)
.insertColumn(TABLE_HOSTS, COLUMN_IP, String.class)
.build(context, name, DB_VERSION);
db = helper.getWritableDatabase();
}
@Nullable
public static InetAddress toInetAddress(String host, String ip) {
if (!isValidHost(host)) {
return null;
}
if (ip == null) {
return null;
}
byte[] bytes = parseV4(ip);
if (bytes == null) {
bytes = parseV6(ip);
}
if (bytes == null) {
return null;
}
try {
return InetAddress.getByAddress(host, bytes);
} catch (UnknownHostException e) {
return null;
}
}
/**
* Returns true if the host is valid.
*/
public static boolean isValidHost(String host) {
if (host == null) {
return false;
}
if (host.length() > 253) {
return false;
}
int labelLength = 0;
for (int i = 0, n = host.length(); i < n; i++) {
char ch = host.charAt(i);
if (ch == '.') {
if (labelLength < 1 || labelLength > 63) {
return false;
}
labelLength = 0;
} else {
labelLength++;
}
if ((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' && ch != '.') {
return false;
}
}
return labelLength >= 1 && labelLength <= 63;
}
/**
* Returns true if ips are valid.
*/
public static boolean isValidIp(String ips) {
if (ips == null) {
return false;
}
String[] list = ips.split("\\+");
for (String ip : list) {
if (parseV4(ip) == null && parseV6(ip) == null) {
return false;
}
}
return true;
}
// org.xbill.DNS.Address.parseV4
@Nullable
private static byte[] parseV4(String s) {
int numDigits;
int currentOctet;
byte[] values = new byte[4];
int currentValue;
int length = s.length();
currentOctet = 0;
currentValue = 0;
numDigits = 0;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
if (c >= '0' && c <= '9') {
/* Can't have more than 3 digits per octet. */
if (numDigits == 3)
return null;
/* Octets shouldn't start with 0, unless they are 0. */
if (numDigits > 0 && currentValue == 0)
return null;
numDigits++;
currentValue *= 10;
currentValue += (c - '0');
/* 255 is the maximum value for an octet. */
if (currentValue > 255)
return null;
} else if (c == '.') {
/* Can't have more than 3 dots. */
if (currentOctet == 3)
return null;
/* Two consecutive dots are bad. */
if (numDigits == 0)
return null;
values[currentOctet++] = (byte) currentValue;
currentValue = 0;
numDigits = 0;
} else
return null;
}
/* Must have 4 octets. */
if (currentOctet != 3)
return null;
/* The fourth octet can't be empty. */
if (numDigits == 0)
return null;
values[currentOctet] = (byte) currentValue;
return values;
}
// org.xbill.DNS.Address.parseV6
@Nullable
private static byte[] parseV6(String s) {
int range = -1;
byte[] data = new byte[16];
String[] tokens = s.split(":", -1);
int first = 0;
int last = tokens.length - 1;
if (tokens[0].length() == 0) {
// If the first two tokens are empty, it means the string
// started with ::, which is fine. If only the first is
// empty, the string started with :, which is bad.
if (last - first > 0 && tokens[1].length() == 0)
first++;
else
return null;
}
if (tokens[last].length() == 0) {
// If the last two tokens are empty, it means the string
// ended with ::, which is fine. If only the last is
// empty, the string ended with :, which is bad.
if (last - first > 0 && tokens[last - 1].length() == 0)
last--;
else
return null;
}
if (last - first + 1 > 8)
return null;
int i, j;
for (i = first, j = 0; i <= last; i++) {
if (tokens[i].length() == 0) {
if (range >= 0)
return null;
range = j;
continue;
}
if (tokens[i].indexOf('.') >= 0) {
// An IPv4 address must be the last component
if (i < last)
return null;
// There can't have been more than 6 components.
if (i > 6)
return null;
byte[] v4addr = parseV4(tokens[i]);
if (v4addr == null)
return null;
for (int k = 0; k < 4; k++)
data[j++] = v4addr[k];
break;
}
try {
for (int k = 0; k < tokens[i].length(); k++) {
char c = tokens[i].charAt(k);
if (Character.digit(c, 16) < 0)
return null;
}
int x = Integer.parseInt(tokens[i], 16);
if (x > 0xFFFF || x < 0)
return null;
data[j++] = (byte) (x >>> 8);
data[j++] = (byte) (x & 0xFF);
} catch (NumberFormatException e) {
return null;
}
}
if (j < 16 && range < 0)
return null;
if (range >= 0) {
int empty = 16 - j;
System.arraycopy(data, range, data, range + empty, j - range);
for (i = range; i < range + empty; i++)
data[i] = 0;
}
return data;
}
/**
* Gets a InetAddress with the host.
*/
@Nullable
public List<InetAddress> get(String host) {
if (!isValidHost(host)) {
return null;
}
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
if (cursor.moveToNext()) {
String[] ips = SqlUtils.getString(cursor, COLUMN_IP, null).split("\\+");
List<InetAddress> addresses = new ArrayList<>();
for (String ip : ips) {
addresses.add(toInetAddress(host, ip));
}
return addresses;
} else {
return null;
}
}
}
public boolean contains(String host) {
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
return cursor.moveToNext();
}
}
/**
* Puts the host-ip pair into this hosts.
*/
public boolean put(String host, String ip) {
if (!isValidHost(host) || !isValidIp(ip)) {
return false;
}
ContentValues values = new ContentValues();
values.put(COLUMN_HOST, host);
values.put(COLUMN_IP, ip);
if (contains(host)) {
db.update(TABLE_HOSTS, values, COLUMN_HOST + " = ?", new String[]{host});
} else {
db.insert(TABLE_HOSTS, null, values);
}
return true;
}
/**
* Puts delete the entry with the host.
*/
public void delete(String host) {
db.delete(TABLE_HOSTS, COLUMN_HOST + " = ?", new String[]{host});
}
/**
* Get all data from this host.
*/
public List<Pair<String, String>> getAll() {
List<Pair<String, String>> result = new ArrayList<>();
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + ";", null)) {
while (cursor.moveToNext()) {
String host = SqlUtils.getString(cursor, COLUMN_HOST, null);
String ip = SqlUtils.getString(cursor, COLUMN_IP, null);
result.add(new Pair<>(host, ip));
}
}
return result;
}
}
+104 -622
View File
@@ -1,641 +1,123 @@
/*
* 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.
*/
@file:Suppress("SameParameterValue")
package com.hippo.ehviewer
import android.content.SharedPreferences
import android.net.Uri
import android.util.Log
import androidx.annotation.DimenRes
import androidx.preference.PreferenceManager
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.yorozuya.LayoutUtils.dp2pix
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.unifile.UniFile
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapMerge
import splitties.preferences.DataStorePreferences
import splitties.preferences.edit
import java.util.Locale
import kotlin.reflect.KProperty
object Settings {
/********************
* Eh
*/
const val KEY_THEME = "theme"
const val KEY_ACCOUNT = "account"
const val KEY_IMAGE_LIMITS = "image_limits"
const val KEY_U_CONFIG = "uconfig"
const val KEY_MY_TAGS = "mytags"
const val KEY_BLACK_DARK_THEME = "black_dark_theme"
const val DEFAULT_BLACK_DARK_THEME = false
const val THEME_SYSTEM = -1
const val KEY_GALLERY_SITE = "gallery_site"
const val KEY_LIST_MODE = "list_mode"
const val KEY_DETAIL_SIZE = "detail_size"
const val KEY_THUMB_SIZE = "thumb_size_"
const val KEY_THUMB_RESOLUTION = "thumb_resolution"
const val KEY_SHOW_JPN_TITLE = "show_jpn_title"
const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
const val KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source"
const val KEY_REQUEST_NEWS = "request_news"
const val KEY_REQUEST_NEWS_TIMER = "request_news_timer"
const val KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour"
const val KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute"
const val KEY_HIDE_HV_EVENTS = "hide_hv_events"
val SIGN_IN_REQUIRED = arrayOf(
KEY_IMAGE_LIMITS,
KEY_GALLERY_SITE,
KEY_U_CONFIG,
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_REQUEST_NEWS_TIMER,
KEY_HIDE_HV_EVENTS,
)
object Settings : DataStorePreferences(null) {
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
/********************
* Privacy and Security
*/
const val KEY_SEC_SECURITY = "enable_secure"
const val VALUE_SEC_SECURITY = false
private val _favFlow = MutableSharedFlow<Unit>()
val favChangesFlow = _favFlow.debounce(1000)
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
/********************
* Download
*/
const val KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme"
const val KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority"
const val KEY_DOWNLOAD_SAVE_PATH = "image_path"
const val KEY_NOTIFICATION_REQUIRED = "notification_required"
const val KEY_DOWNLOAD_SAVE_QUERY = "image_query"
const val KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment"
const val KEY_MEDIA_SCAN = "media_scan"
const val INVALID_DEFAULT_FAV_SLOT = -2
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
var downloadPath by stringOrNullPref("image_path", null)
var downloadQuery by stringOrNullPref("image_query", null)
var downloadFragment by stringOrNullPref("image_fragment", null)
var archivePasswds by stringSetOrNullPref("archive_passwds")
var downloadDelay by intFromStrPref("download_delay", 0)
var gallerySite by intFromStrPref("gallery_site", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intFromStrPref("download_thread", 3)
var preloadImage by intFromStrPref("preload_image", 5)
var theme by intFromStrPref("theme", -1).observed { updateWhenThemeChanges() }
var listMode by intFromStrPref("list_mode", 0)
var detailSize by intFromStrPref("detail_size", 0)
var thumbResolution by intFromStrPref("thumb_resolution", 0)
var readCacheSize by intFromStrPref("read_cache_size", 640)
var launchPage by intFromStrPref("launch_page", 0)
var forceEhThumb by boolPref("force_eh_thumb", false)
var showComments by boolPref("show_gallery_comments", true)
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
var hideHvEvents by boolPref("hide_hv_events", false)
var showJpnTitle by boolPref("show_jpn_title", false)
var showGalleryPages by boolPref("show_gallery_pages", false)
var showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
var meteredNetworkWarning by boolPref("cellular_network_warning", false)
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
var enabledSecurity by boolPref("enable_secure", false)
var useCIUpdateChannel by boolPref("ci_update_channel", false)
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
var qSSaveProgress by boolPref("qs_save_progress", true)
var saveParseErrorBody by boolPref("save_parse_error_body", true)
var saveCrashLog by boolPref("save_crash_log", true)
var security by boolPref("require_unlock", false)
var removeImageFiles by boolPref("include_pic", true)
var needSignIn by boolPref("need_sign_in", true)
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
var downloadOriginImage by boolPref("download_origin_image", false)
var thumbSizeDp by intPref("thumb_size_", 120)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
var securityDelay by intPref("require_unlock_delay", 0)
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
var listThumbSize by intPref("list_tile_size", 40)
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
var updateIntervalDays by intPref("update_interval_days", 0)
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var displayName by stringOrNullPref("display_name", null)
var avatar by stringOrNullPref("avatar", null)
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
var lastDawnDay by longPref("last_dawn_day", 0)
var lastUpdateDay by longPref("last_update_day", 0)
/********************
* Advanced
*/
const val KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body"
const val KEY_SECURITY = "require_unlock"
const val KEY_SECURITY_DELAY = "require_unlock_delay"
const val KEY_READ_CACHE_SIZE = "read_cache_size"
const val DEFAULT_READ_CACHE_SIZE = 640
const val KEY_BUILT_IN_HOSTS = "built_in_hosts_2"
const val KEY_DOMAIN_FRONTING = "domain_fronting"
const val KEY_BYPASS_VPN = "bypass_vpn"
const val KEY_LIST_THUMB_SIZE = "list_tile_size"
private const val KEY_LAST_DAWN_DAY = "last_dawn_day"
private const val DEFAULT_HIDE_HV_EVENTS = false
private const val KEY_SHOW_COMMENTS = "show_gallery_comments"
private const val DEFAULT_SHOW_COMMENTS = true
private val TAG = Settings::class.java.simpleName
private const val KEY_DISPLAY_NAME = "display_name"
private val DEFAULT_DISPLAY_NAME: String? = null
private const val DEFAULT_LIST_THUMB_SIZE = 40
private const val KEY_AVATAR = "avatar"
private const val KEY_REMOVE_IMAGE_FILES = "include_pic"
private const val DEFAULT_REMOVE_IMAGE_FILES = true
private const val KEY_NEED_SIGN_IN = "need_sign_in"
private const val DEFAULT_NEED_SIGN_IN = true
private const val KEY_SELECT_SITE = "select_site"
private const val DEFAULT_SELECT_SITE = true
private const val DEFAULT_THEME = THEME_SYSTEM
private const val DEFAULT_GALLERY_SITE = 0
private const val KEY_LAUNCH_PAGE = "launch_page"
private const val DEFAULT_LAUNCH_PAGE = 0
private const val DEFAULT_LIST_MODE = 0
private const val DEFAULT_DETAIL_SIZE = 0
private const val DEFAULT_THUMB_SIZE = 120
private const val DEFAULT_THUMB_RESOLUTION = 0
private const val DEFAULT_SHOW_JPN_TITLE = false
private const val KEY_SHOW_GALLERY_PAGES = "show_gallery_pages"
private const val DEFAULT_SHOW_GALLERY_PAGES = false
private const val DEFAULT_SHOW_TAG_TRANSLATIONS = false
private const val KEY_METERED_NETWORK_WARNING = "cellular_network_warning"
private const val DEFAULT_METERED_NETWORK_WARNING = false
private const val KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip"
private const val DEFAULT_APP_LINK_VERIFY_TIP = false
private const val DEFAULT_MEDIA_SCAN = false
private const val KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label"
private val DEFAULT_RECENT_DOWNLOAD_LABEL: String? = null
private const val KEY_HAS_DEFAULT_DOWNLOAD_LABEL = "has_default_download_label"
private const val DEFAULT_HAS_DOWNLOAD_LABEL = false
private const val KEY_DEFAULT_DOWNLOAD_LABEL = "default_download_label"
private val DEFAULT_DOWNLOAD_LABEL: String? = null
private const val KEY_MULTI_THREAD_DOWNLOAD = "download_thread"
private const val DEFAULT_MULTI_THREAD_DOWNLOAD = 3
private const val KEY_PRELOAD_IMAGE = "preload_image"
private const val DEFAULT_PRELOAD_IMAGE = 5
private const val KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image"
private const val DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false
/********************
* Favorites
*/
private const val KEY_FAV_CAT_0 = "fav_cat_0"
private const val KEY_FAV_CAT_1 = "fav_cat_1"
private const val KEY_FAV_CAT_2 = "fav_cat_2"
private const val KEY_FAV_CAT_3 = "fav_cat_3"
private const val KEY_FAV_CAT_4 = "fav_cat_4"
private const val KEY_FAV_CAT_5 = "fav_cat_5"
private const val KEY_FAV_CAT_6 = "fav_cat_6"
private const val KEY_FAV_CAT_7 = "fav_cat_7"
private const val KEY_FAV_CAT_8 = "fav_cat_8"
private const val KEY_FAV_CAT_9 = "fav_cat_9"
private const val DEFAULT_FAV_CAT_0 = "Favorites 0"
private const val DEFAULT_FAV_CAT_1 = "Favorites 1"
private const val DEFAULT_FAV_CAT_2 = "Favorites 2"
private const val DEFAULT_FAV_CAT_3 = "Favorites 3"
private const val DEFAULT_FAV_CAT_4 = "Favorites 4"
private const val DEFAULT_FAV_CAT_5 = "Favorites 5"
private const val DEFAULT_FAV_CAT_6 = "Favorites 6"
private const val DEFAULT_FAV_CAT_7 = "Favorites 7"
private const val DEFAULT_FAV_CAT_8 = "Favorites 8"
private const val DEFAULT_FAV_CAT_9 = "Favorites 9"
private const val KEY_FAV_COUNT_0 = "fav_count_0"
private const val KEY_FAV_COUNT_1 = "fav_count_1"
private const val KEY_FAV_COUNT_2 = "fav_count_2"
private const val KEY_FAV_COUNT_3 = "fav_count_3"
private const val KEY_FAV_COUNT_4 = "fav_count_4"
private const val KEY_FAV_COUNT_5 = "fav_count_5"
private const val KEY_FAV_COUNT_6 = "fav_count_6"
private const val KEY_FAV_COUNT_7 = "fav_count_7"
private const val KEY_FAV_COUNT_8 = "fav_count_8"
private const val KEY_FAV_COUNT_9 = "fav_count_9"
private const val KEY_FAV_LOCAL = "fav_local"
private const val KEY_FAV_CLOUD = "fav_cloud"
private const val DEFAULT_FAV_COUNT = 0
private const val KEY_RECENT_FAV_CAT = "recent_fav_cat"
private const val DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
private const val KEY_DEFAULT_FAV_SLOT = "default_favorite_2"
private const val DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT
private const val DEFAULT_SAVE_PARSE_ERROR_BODY = true
private const val KEY_SAVE_CRASH_LOG = "save_crash_log"
private const val DEFAULT_SAVE_CRASH_LOG = true
private const val DEFAULT_BUILT_IN_HOSTS = false
private const val DEFAULT_FRONTING = false
private const val DEFAULT_BYPASS_VPN = true
private const val KEY_PROXY_TYPE = "proxy_type"
private const val DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM
private const val KEY_PROXY_IP = "proxy_ip"
private val DEFAULT_PROXY_IP: String? = null
private const val KEY_PROXY_PORT = "proxy_port"
private const val DEFAULT_PROXY_PORT = -1
private const val KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code"
private const val DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0
private const val KEY_DOWNLOAD_DELAY = "download_delay"
private const val DEFAULT_DOWNLOAD_DELAY = 0
private const val DEFAULT_REQUEST_NEWS = false
private const val KEY_ARCHIVE_PASSWDS = "archive_passwds"
private const val KEY_QS_SAVE_PROGRESS = "qs_save_progress"
private const val DEFAULT_QS_SAVE_PROGRESS = true
private const val KEY_PRELOAD_THUMB_AGGRESIVELY = "preload_thumb_aggressively"
private const val DEFAULT_PRELOAD_THUMB_AGGRESIVELY = false
var LIST_THUMB_SIZE_INITED = false
private lateinit var sSettingsPre: SharedPreferences
private var LIST_THUMB_SIZE = 40
fun initialize() {
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(application)
fixDefaultValue()
}
private fun fixDefaultValue() {
init {
if ("CN" == Locale.getDefault().country) {
// Enable domain fronting if the country is CN
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
putBuiltInHosts(true)
}
if (!sSettingsPre.contains(KEY_DOMAIN_FRONTING)) {
putDF(true)
}
// Enable show tag translations if the country is CN
if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {
putShowTagTranslations(true)
edit {
if (KEY_SHOW_TAG_TRANSLATIONS !in prefs) showTagTranslations = true
}
}
}
private fun getBoolean(key: String, defValue: Boolean): Boolean {
return try {
sSettingsPre.getBoolean(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
interface Delegate<R> {
val flowGetter: () -> Flow<Unit>
operator fun getValue(thisRef: Any?, prop: KProperty<*>?): R
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
}
private fun intFromStrPref(key: String, defValue: Int) = object : Delegate<Int> {
override val flowGetter: () -> Flow<Unit>
private var _value by stringPref(key, defValue.toString()).also { flowGetter = { it.changesFlow() } }
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = _value.toIntOrNull() ?: defValue
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Int) { _value = value.toString() }
}
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
}
}
private fun putBoolean(key: String, value: Boolean) {
sSettingsPre.edit().putBoolean(key, value).apply()
}
@JvmStatic
fun getInt(key: String, defValue: Int): Int {
return try {
sSettingsPre.getInt(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
}
}
@JvmStatic
fun putInt(key: String, value: Int) {
sSettingsPre.edit().putInt(key, value).apply()
}
private fun getString(key: String, defValue: String?): String? {
return try {
sSettingsPre.getString(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
private fun putString(key: String, value: String?) {
sSettingsPre.edit().putString(key, value).apply()
}
private fun getStringSet(key: String): MutableSet<String>? {
return sSettingsPre.getStringSet(key, null)
}
private fun putStringToStringSet(key: String, value: String) {
var set = getStringSet(key)
if (set == null) {
set =
mutableSetOf(value)
} else if (set.contains(value)) return else set.add(value)
sSettingsPre.edit().putStringSet(key, set).apply()
}
private fun getIntFromStr(key: String, defValue: Int): Int {
return try {
NumberUtils.parseIntSafely(
sSettingsPre.getString(key, defValue.toString()),
defValue,
)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
private fun putIntToStr(key: String, value: Int) {
sSettingsPre.edit().putString(key, value.toString()).apply()
}
var lastDawnDay: Long
get() = sSettingsPre.getLong(KEY_LAST_DAWN_DAY, 0)
set(value) = sSettingsPre.edit().putLong(KEY_LAST_DAWN_DAY, value).apply()
val displayName: String?
get() = getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME)
fun putDisplayName(value: String?) {
putString(KEY_DISPLAY_NAME, value)
}
fun putAvatar(value: String?) {
putString(KEY_AVATAR, value)
}
val removeImageFiles: Boolean
get() = getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES)
fun putRemoveImageFiles(value: Boolean) {
putBoolean(KEY_REMOVE_IMAGE_FILES, value)
}
val needSignIn: Boolean
get() = getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN)
fun putNeedSignIn(value: Boolean) {
putBoolean(KEY_NEED_SIGN_IN, value)
}
val selectSite: Boolean
get() = getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE)
fun putSelectSite(value: Boolean) {
putBoolean(KEY_SELECT_SITE, value)
}
val theme: Int
get() = getIntFromStr(KEY_THEME, DEFAULT_THEME)
val blackDarkTheme
get() = getBoolean(KEY_BLACK_DARK_THEME, DEFAULT_BLACK_DARK_THEME)
val gallerySite: Int
get() = getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE)
fun putGallerySite(value: Int) {
putIntToStr(KEY_GALLERY_SITE, value)
}
val launchPageGalleryListSceneAction: String
get() {
return when (val value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE)) {
0 -> GalleryListScene.ACTION_HOMEPAGE
1 -> GalleryListScene.ACTION_SUBSCRIPTION
2 -> GalleryListScene.ACTION_WHATS_HOT
3 -> GalleryListScene.ACTION_TOP_LIST
else -> throw IllegalStateException("Unexpected value: $value")
}
}
val listMode: Int
get() = getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE)
private val detailSize: Int
get() = getIntFromStr(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE)
@get:DimenRes
val detailSizeResId: Int
get() = when (detailSize) {
0 -> R.dimen.gallery_list_column_width_long
1 -> R.dimen.gallery_list_column_width_short
else -> throw IllegalStateException("Unexpected value: $detailSize")
}
val thumbSize: Int
get() = dp2pix(application, thumbSizeDp.toFloat())
val thumbSizeDp: Int
get() = getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE)
val thumbResolution: Int
get() = getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION)
val showComments: Boolean
get() = getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS)
val requestNews: Boolean
get() = getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS)
val hideHvEvents: Boolean
get() = getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS)
val showJpnTitle: Boolean
get() = getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE)
val showGalleryPages: Boolean
get() = getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES)
val showTagTranslations: Boolean
get() = getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS)
private fun putShowTagTranslations(value: Boolean) {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value)
}
val meteredNetworkWarning: Boolean
get() = getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING)
val appLinkVerifyTip: Boolean
get() = getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP)
fun putAppLinkVerifyTip(value: Boolean) {
putBoolean(KEY_APP_LINK_VERIFY_TIP, value)
}
val enabledSecurity: Boolean
get() = getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY)
val downloadLocation: UniFile?
get() {
val dir: UniFile?
val builder = Uri.Builder()
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null))
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null))
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null))
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null))
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null))
dir = UniFile.fromUri(application, builder.build())
return dir ?: UniFile.fromFile(AppConfig.getDefaultDownloadDir())
}
fun putDownloadLocation(location: UniFile) {
val uri = location.uri
putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.scheme)
putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.encodedAuthority)
putString(KEY_DOWNLOAD_SAVE_PATH, uri.encodedPath)
putString(KEY_DOWNLOAD_SAVE_QUERY, uri.encodedQuery)
putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.encodedFragment)
}
val mediaScan: Boolean
get() = getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN)
val recentDownloadLabel: String?
get() = getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL)
fun putRecentDownloadLabel(value: String?) {
putString(KEY_RECENT_DOWNLOAD_LABEL, value)
}
val hasDefaultDownloadLabel: Boolean
get() = getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL)
fun putHasDefaultDownloadLabel(hasDefaultDownloadLabel: Boolean) {
putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, hasDefaultDownloadLabel)
}
val defaultDownloadLabel: String?
get() = getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL)
fun putDefaultDownloadLabel(value: String?) {
putString(KEY_DEFAULT_DOWNLOAD_LABEL, value)
}
val multiThreadDownload: Int
get() = getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD)
val downloadDelay: Int
get() = getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY)
val preloadImage: Int
get() = getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE)
val downloadOriginImage: Boolean
get() = getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE)
val favCat: Array<String>
get() = arrayOf(
sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0)!!,
sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1)!!,
sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2)!!,
sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3)!!,
sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4)!!,
sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5)!!,
sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6)!!,
sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7)!!,
sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8)!!,
sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9)!!,
)
fun putFavCat(value: Array<String?>) {
check(value.size == 10)
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
.putString(KEY_FAV_CAT_2, value[2])
.putString(KEY_FAV_CAT_3, value[3])
.putString(KEY_FAV_CAT_4, value[4])
.putString(KEY_FAV_CAT_5, value[5])
.putString(KEY_FAV_CAT_6, value[6])
.putString(KEY_FAV_CAT_7, value[7])
.putString(KEY_FAV_CAT_8, value[8])
.putString(KEY_FAV_CAT_9, value[9])
.apply()
}
val favCount: IntArray
get() = intArrayOf(
sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT),
)
fun putFavCount(count: IntArray) {
check(count.size == 10)
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
.putInt(KEY_FAV_COUNT_2, count[2])
.putInt(KEY_FAV_COUNT_3, count[3])
.putInt(KEY_FAV_COUNT_4, count[4])
.putInt(KEY_FAV_COUNT_5, count[5])
.putInt(KEY_FAV_COUNT_6, count[6])
.putInt(KEY_FAV_COUNT_7, count[7])
.putInt(KEY_FAV_COUNT_8, count[8])
.putInt(KEY_FAV_COUNT_9, count[9])
.apply()
}
val favLocalCount: Int
get() = sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT)
fun putFavLocalCount(count: Int) {
sSettingsPre.edit().putInt(KEY_FAV_LOCAL, count).apply()
}
val favCloudCount: Int
get() = sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT)
fun putFavCloudCount(count: Int) {
sSettingsPre.edit().putInt(KEY_FAV_CLOUD, count).apply()
}
val recentFavCat: Int
get() = getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT)
fun putRecentFavCat(value: Int) {
putInt(KEY_RECENT_FAV_CAT, value)
}
val defaultFavSlot: Int
get() = getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT)
fun putDefaultFavSlot(value: Int) {
putInt(KEY_DEFAULT_FAV_SLOT, value)
}
val qSSaveProgress: Boolean
get() = getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS)
fun putQSSaveProgress(value: Boolean) {
putBoolean(KEY_QS_SAVE_PROGRESS, value)
}
val saveParseErrorBody: Boolean
get() = getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY)
val saveCrashLog: Boolean
get() = getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG)
val security: Boolean
get() = getBoolean(KEY_SECURITY, false)
fun putSecurity(value: Boolean) {
putBoolean(KEY_SECURITY, value)
}
val securityDelay: Int
get() = getInt(KEY_SECURITY_DELAY, 0)
val readCacheSize: Int
get() = getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE)
val builtInHosts: Boolean
get() = getBoolean(KEY_BUILT_IN_HOSTS, DEFAULT_BUILT_IN_HOSTS)
private fun putBuiltInHosts(value: Boolean) {
putBoolean(KEY_BUILT_IN_HOSTS, value)
}
val dF: Boolean
get() = getBoolean(KEY_DOMAIN_FRONTING, DEFAULT_FRONTING)
private fun putDF(value: Boolean) {
putBoolean(KEY_DOMAIN_FRONTING, value)
}
val bypassVpn: Boolean
get() = getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN)
@JvmStatic
val proxyType: Int
get() = getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE)
fun putProxyType(value: Int) {
putInt(KEY_PROXY_TYPE, value)
}
@JvmStatic
val proxyIp: String?
get() = getString(KEY_PROXY_IP, DEFAULT_PROXY_IP)
fun putProxyIp(value: String?) {
putString(KEY_PROXY_IP, value)
}
@JvmStatic
val proxyPort: Int
get() = getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT)
fun putProxyPort(value: Int) {
putInt(KEY_PROXY_PORT, value)
}
val clipboardTextHashCode: Int
get() = getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE)
fun putClipboardTextHashCode(value: Int) {
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value)
}
val archivePasswds: Set<String>?
get() = getStringSet(KEY_ARCHIVE_PASSWDS)
fun putPasswdToArchivePasswds(value: String) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value)
}
val listThumbSize: Int
get() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE
}
val size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE)
LIST_THUMB_SIZE = size
LIST_THUMB_SIZE_INITED = true
return size
}
val notificationRequired: Boolean
get() = getBoolean(KEY_NOTIFICATION_REQUIRED, false)
fun putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true)
}
val preloadThumbAggressively: Boolean
get() = getBoolean(KEY_PRELOAD_THUMB_AGGRESIVELY, DEFAULT_PRELOAD_THUMB_AGGRESIVELY)
}
@@ -0,0 +1,83 @@
package com.hippo.ehviewer
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
import androidx.core.os.LocaleListCompat
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.preferences.PrefDelegate
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
fun <T, R : Settings.Delegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { flowGetter().collect(func) } }
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flowGetter().collect { flow.emit(Unit) } } }
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { changesFlow().collect { flow.emit(Unit) } } }
fun updateWhenLocaleChanges() {
val newValue = Settings.language
if ("system" == newValue) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
} else {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue))
}
}
fun updateWhenKeepMediaStatusChanges() {
collectScope.launchIO {
runCatching {
keepNoMediaFileStatus()
}
}
}
fun updateWhenThemeChanges() {
collectScope.launch {
delay(100) // Avoid recompose being cancelled
AppCompatDelegate.setDefaultNightMode(Settings.theme)
}
}
fun updateWhenAmoledModeChanges() {
collectScope.launch {
delay(100) // Avoid recompose being cancelled
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY)
AppCompatDelegate.setDefaultNightMode(Settings.theme)
}
}
fun updateWhenRequestNewsChanges() {
updateDailyCheckWork(appCtx)
}
fun updateWhenGallerySiteChanges() {
if (!Settings.needSignIn && EhCookieStore.hasSignedIn()) {
collectScope.launchIO {
runCatching {
EhEngine.getUConfig()
}.onFailure {
it.printStackTrace()
}
}
}
}
fun updateWhenTagTranslationChanges() {
collectScope.launchIO {
runCatching {
EhTagDatabase.update()
}.onFailure {
it.printStackTrace()
}
}
}
@@ -15,6 +15,7 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.NormalGalleryPreview
@@ -26,21 +27,24 @@ import com.hippo.ehviewer.client.data.NormalGalleryPreview
private const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
private const val URL_PREFIX_LARGE_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
fun getImageKey(gid: Long, index: Int): String {
return "image:$gid:$index"
}
fun getThumbKey(url: String): String {
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_THUMB_EX).removePrefix("t/")
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
}
val GalleryPreview.url
get() = if (this is NormalGalleryPreview) {
if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + imageKey else URL_PREFIX_THUMB_E + imageKey
get() = if (exThumb) {
if (this is NormalGalleryPreview) URL_PREFIX_THUMB_EX else URL_PREFIX_LARGE_THUMB_EX
} else {
if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + "t/" + imageKey else URL_PREFIX_THUMB_E + imageKey
}
URL_PREFIX_THUMB_E
} + imageKey
val GalleryInfo.thumbUrl
get() = if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + "t/" + thumbKey!! else URL_PREFIX_THUMB_E + thumbKey!!
get() = (if (exThumb) URL_PREFIX_LARGE_THUMB_EX else URL_PREFIX_THUMB_E) + thumbKey!!
val exThumb get() = EhUtils.isExHentai && !Settings.forceEhThumb
@@ -15,136 +15,65 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.network.CookieDatabase
import com.hippo.ehviewer.network.CookieSet
import eu.kanade.tachiyomi.util.lang.launchIO
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(EhApplication.application)
private val map: MutableMap<String, CookieSet> = db.allCookies
private val manager = CookieManager.getInstance()
fun signOut() = manager.removeAllCookies(null)
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
fun signOut() {
clear()
fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString())
return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
} else {
emptyList()
}
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPB_MEMBER_ID) &&
contains(url, KEY_IPB_PASS_HASH)
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
}
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
const val KEY_SETTINGS_PROFILE = "sp"
const val KEY_STAR = "star"
private const val KEY_STAR = "star"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(KEY_CONTENT_WARNING)
.value(CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
private const val KEY_UTMP_NAME = "__utmp"
private val sTipsCookie = Cookie.Builder().apply {
name(KEY_CONTENT_WARNING)
value(CONTENT_WARNING_NOT_SHOW)
domain(EhUrl.DOMAIN_E)
path("/")
expiresAt(Long.MAX_VALUE)
}.build()
fun newCookie(
cookie: Cookie,
newDomain: String,
forcePersistent: Boolean = false,
forceLongLive: Boolean = false,
forceNotHostOnly: Boolean = false,
): Cookie {
val builder = Cookie.Builder()
builder.name(cookie.name)
builder.value(cookie.value)
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE)
} else if (cookie.persistent) {
builder.expiresAt(cookie.expiresAt)
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE)
}
if (cookie.hostOnly && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain)
} else {
builder.domain(newDomain)
}
builder.path(cookie.path)
if (cookie.secure) {
builder.secure()
}
if (cookie.httpOnly) {
builder.httpOnly()
}
return builder.build()
}
fun copyCookie(domain: String, newDomain: String, name: String, path: String = "/") {
val cookie = map[domain]?.get(name, domain, path)
cookie?.let { addCookie(newCookie(it, newDomain)) }
fun copyNecessaryCookies() {
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
flush()
}
fun deleteCookie(url: HttpUrl, name: String) {
val deletedCookie = Cookie.Builder()
.name(name)
.value("deleted")
.domain(url.host)
.expiresAt(0)
.build()
addCookie(deletedCookie)
manager.setCookie(url.toString(), "$name=;Max-Age=0")
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
}
fun flush() = manager.flush()
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookies = loadForRequest(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
@@ -159,111 +88,17 @@ object EhCookieStore : CookieJar {
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
launchIO {
db.clear()
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
}
}
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else {
urlHost.endsWith(domain!!) &&
urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost,
)
}
// As in 'example.com' matching 'www.example.com'.
}
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }.also { flush() }
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (KEY_CONTENT_WARNING == name) {
continue
}
result.add(cookie)
val checkTips = EhUrl.DOMAIN_E in url.host
return get(url).run {
if (checkTips) {
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
} else {
this
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
}
@@ -1,137 +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
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
object EhDns : Dns {
private val hosts = EhApplication.hosts
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", false),
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,
"s.exhentai.org",
Pair("178.175.129.254", false),
Pair("178.175.128.254", 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.dF && pair.second }
}
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.builtInHosts }
?: Dns.SYSTEM.lookup(hostname)
}
fun isInHosts(hostname: String): Boolean {
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.builtInHosts)
}
}
@@ -18,12 +18,13 @@ package com.hippo.ehviewer.client
import android.util.Log
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.parZip
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.EventPaneParser
@@ -38,16 +39,27 @@ import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.client.parser.ProfileParser
import com.hippo.ehviewer.client.parser.RateGalleryParser
import com.hippo.ehviewer.client.parser.RateGalleryResult
import com.hippo.ehviewer.client.parser.SignInParser
import com.hippo.ehviewer.client.parser.TorrentParser
import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.client.parser.TorrentResult
import com.hippo.ehviewer.client.parser.VoteCommentResult
import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.ehviewer.cronet.awaitBodyFully
import com.hippo.ehviewer.cronet.cronetRequest
import com.hippo.ehviewer.cronet.execute
import com.hippo.ehviewer.dailycheck.showEventNotification
import com.hippo.ehviewer.dailycheck.today
import com.hippo.ehviewer.network.StatusCodeException
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.StatusCodeException
import com.hippo.ehviewer.util.isCronetSupported
import io.ktor.utils.io.pool.DirectByteBufferPool
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put
import moe.tarsin.coroutines.removeAllSuspend
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.Headers
import okhttp3.MediaType
@@ -55,9 +67,10 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import org.json.JSONArray
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.nio.ByteBuffer
import kotlin.math.ceil
private const val TAG = "EhEngine"
@@ -85,7 +98,7 @@ private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwab
// Check kokomade
if (body.contains(KOKOMADE_URL)) {
throw EhException("今回はここまで ${GetText.getString(R.string.kokomade_tip)}".trimIndent())
throw EhException("今回はここまで ${appCtx.getString(R.string.kokomade_tip)}".trimIndent())
}
// Check Gallery Not Available
@@ -103,12 +116,12 @@ private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwab
if (e is ParseException) {
if (body.isEmpty()) {
throw EhException(GetText.getString(R.string.error_empty_html))
throw EhException(appCtx.getString(R.string.error_empty_html))
} else if ("<" !in body) {
throw EhException(body)
} else {
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
throw EhException(GetText.getString(R.string.error_parse_error))
throw EhException(appCtx.getString(R.string.error_parse_error))
}
}
@@ -116,7 +129,40 @@ private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwab
throw e
}
private suspend inline fun <T> Request.executeAndParsingWith(block: String.() -> T): T {
val httpContentPool = DirectByteBufferPool(16, 16384)
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
return if (isCronetSupported) {
cronetRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
awaitBodyFully {
buffer.put(it)
}
buffer.flip()
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
} else {
ehRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
while (true) {
if (body.source().read(buffer) == -1) break
}
buffer.flip()
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
}
}
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
private suspend inline fun <T> Request.executeAndParsingWith(block: suspend String.() -> T): T {
Log.d(TAG, url.toString())
return execute {
val body = body.string()
@@ -129,9 +175,20 @@ private suspend inline fun <T> Request.executeAndParsingWith(block: String.() ->
}
object EhEngine {
suspend fun getTorrentList(url: String, gid: Long, token: String?): List<TorrentParser.Result> {
suspend fun getOriginalImageUrl(url: String, referer: String?): String {
Log.d(TAG, url)
return ehRequest(url, referer).executeNoRedirect {
header("Location")?.apply {
if (contains("bounce_login")) {
throw NotLoggedInException()
}
} ?: throw InsufficientFundsException()
}
}
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
return ehRequest(url, referer).executeAndParsingWith(TorrentParser::parse)
return fetchCompat(url, referer, TorrentParser::parse)
}
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
@@ -139,7 +196,7 @@ object EhEngine {
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) }
suspend fun getImageLimits() = parZip(
{ ehRequest(EhUrl.URL_HOME).executeAndParsingWith(HomeParser::parse) },
{ fetchCompat(EhUrl.URL_HOME, parser = HomeParser::parse) },
{ ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) },
{ limits, funds -> HomeParser.Result(limits, funds) },
)
@@ -154,12 +211,12 @@ object EhEngine {
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
runSuspendCatching {
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "U_CONFIG_TEXT not found!" } }
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
}.onFailure {
// It may get redirected when accessing ex for the first time
if (EhUtils.isExHentai) {
if (url == EhUrl.URL_UCONFIG_EX) {
it.printStackTrace()
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "U_CONFIG_TEXT not found!" } }
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
} else {
throw it
}
@@ -233,11 +290,11 @@ object EhEngine {
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
* @param note max 250 characters
*/
suspend fun addFavorites(
suspend fun modifyFavorites(
gid: Long,
token: String?,
dstCat: Int,
note: String?,
dstCat: Int = -1,
note: String = "",
) {
val catStr: String = when (dstCat) {
-1 -> "favdel"
@@ -248,9 +305,9 @@ object EhEngine {
return ehRequest(url, url, EhUrl.origin) {
formBody {
add("favcat", catStr)
add("favnote", note ?: "")
// submit=Add+to+Favorites is not necessary, just use submit=Apply+Changes all the time
add("submit", "Apply Changes")
add("favnote", note)
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
add("apply", "Apply Changes")
add("update", "1")
}
}.executeAndParsingWith { }
@@ -300,14 +357,11 @@ object EhEngine {
return null
}
suspend fun resetImageLimits(): HomeParser.Limits? {
return ehRequest(EhUrl.URL_HOME) {
formBody {
add("act", "limits")
add("reset", "Reset Limit")
}
}.executeAndParsingWith(HomeParser::parseResetLimits)
}
suspend fun resetImageLimits() = ehRequest(EhUrl.URL_HOME) {
formBody {
add("reset_imagelimit", "Reset Limit")
}
}.executeAndParsingWith(HomeParser::parseResetLimits)
suspend fun modifyFavorites(url: String, gidArray: LongArray, dstCat: Int): FavoritesParser.Result {
val catStr: String = when (dstCat) {
@@ -319,7 +373,6 @@ object EhEngine {
formBody {
add("ddact", catStr)
gidArray.forEach { add("modifygids[]", it.toString()) }
add("apply", "Apply")
}
}.executeAndParsingWith(FavoritesParser::parse).apply { fillGalleryList(galleryInfoList, url, false) }
}
@@ -343,7 +396,13 @@ object EhEngine {
}.executeAndParsingWith(GalleryPageApiParser::parse)
}
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String?, rating: Float) = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
suspend fun rateGallery(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
rating: Float,
): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
jsonBody {
put("method", "rategallery")
put("apiuid", apiUid)
@@ -352,13 +411,20 @@ object EhEngine {
put("token", token)
put("rating", ceil((rating * 2).toDouble()).toInt())
}
}.executeAndParsingWith(RateGalleryParser::parse)
}.executeAndParsingWith(String::parseAs)
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) = galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap {
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
put("method", "gdata")
array("gidlist") { it.forEach { put(jsonArrayOf(it.gid, it.token)) } }
array("gidlist") {
it.forEach {
addJsonArray {
add(it.gid)
add(it.token)
}
}
}
put("namespace", 1)
}
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
@@ -371,7 +437,7 @@ object EhEngine {
token: String?,
commentId: Long,
commentVote: Int,
) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
): VoteCommentResult = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
jsonBody {
put("method", "votecomment")
put("apiuid", apiUid)
@@ -381,7 +447,7 @@ object EhEngine {
put("comment_id", commentId)
put("comment_vote", commentVote)
}
}.executeAndParsingWith { VoteCommentParser.parse(this, commentVote) }
}.executeAndParsingWith(String::parseAs)
suspend fun voteTag(
apiUid: Long,
@@ -405,7 +471,13 @@ object EhEngine {
suspend fun getGalleryToken(gid: Long, gtoken: String?, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
jsonBody {
put("method", "gtoken")
put("pagelist", JSONArray().put(JSONArray().put(gid).put(gtoken).put(page + 1)))
array("pagelist") {
addJsonArray {
add(gid)
add(gtoken)
add(page + 1)
}
}
}
}.executeAndParsingWith(GalleryTokenApiParser::parse)
@@ -422,9 +494,9 @@ object EhEngine {
}
}.executeAndParsingWith(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl, true) }
private suspend fun fillGalleryList(list: MutableList<GalleryInfo>, url: String, filter: Boolean) {
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean) {
// Filter title and uploader
if (filter) list.removeAll { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
var hasTags = false
var hasPages = false
@@ -444,11 +516,10 @@ object EhEngine {
if (needApi) fillGalleryListByApi(list, url)
// Filter tag, thumbnail mode need filter uploader again
if (filter) list.removeAll { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
}
suspend fun addFavoritesRange(gidArray: LongArray, tokenArray: Array<String?>, dstCat: Int) {
require(gidArray.size == tokenArray.size)
gidArray.forEachIndexed { index, gid -> addFavorites(gid, tokenArray[index], dstCat, null) }
suspend fun modifyFavoritesRange(galleryList: List<Pair<Long, String>>, dstCat: Int) {
galleryList.forEach { (gid, token) -> modifyFavorites(gid, token, dstCat) }
}
}
@@ -1,112 +1,35 @@
/*
* 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.client
import android.util.Log
import arrow.core.memoize
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
private val regex = { p: Filter -> Regex(p.text!!) }.memoize()
private val regex = { p: Filter -> Regex(p.text) }.memoize()
object EhFilter {
val titleFilterList = mutableListOf<Filter>()
val uploaderFilterList = mutableListOf<Filter>()
val tagFilterList = mutableListOf<Filter>()
val tagNamespaceFilterList = mutableListOf<Filter>()
val commenterFilterList = mutableListOf<Filter>()
val commentFilterList = mutableListOf<Filter>()
const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
const val MODE_TAG = 2
const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
const val MODE_COMMENT = 5
private const val TAG = "EhFilter"
init { EhDB.allFilter.forEach(::memorizeFilter) }
private fun memorizeFilter(filter: Filter) {
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase()
titleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase()
tagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase()
tagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> uploaderFilterList.add(filter)
MODE_COMMENTER -> commenterFilterList.add(filter)
MODE_COMMENT -> commentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
object EhFilter : CoroutineScope {
override val coroutineContext = Dispatchers.IO.limitedParallelism(1)
val filters = async { EhDB.getAllFilter() as MutableList }
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
private fun <R> Filter.launchOps(
callback: ((R) -> Unit)? = null,
ops: suspend Filter.() -> R,
) = launch { ops().let { callback?.invoke(it) } }
fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) {
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
}
@Synchronized
fun addFilter(filter: Filter): Boolean {
filter.enable = true
if (!EhDB.addFilter(filter)) return false
memorizeFilter(filter)
return true
fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
enable = !enable
EhDB.updateFilter(this)
}
@Synchronized
fun triggerFilter(filter: Filter) {
EhDB.triggerFilter(filter)
}
@Synchronized
fun deleteFilter(filter: Filter) {
EhDB.deleteFilter(filter)
when (filter.mode) {
MODE_TITLE -> titleFilterList.remove(filter)
MODE_TAG -> tagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> tagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> uploaderFilterList.remove(filter)
MODE_COMMENTER -> commenterFilterList.remove(filter)
MODE_COMMENT -> commentFilterList.remove(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean {
return 0 != tagFilterList.size || 0 != tagNamespaceFilterList.size
}
@Synchronized
fun filterTitle(info: GalleryInfo): Boolean {
val title = info.title ?: return false
return titleFilterList.any { it.enable!! && it.text!! in title.lowercase() }
}
@Synchronized
fun filterUploader(info: GalleryInfo): Boolean {
val uploader = info.uploader ?: return false
return uploaderFilterList.any { it.enable!! && it.text == uploader }
fun Filter.forget(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
EhDB.deleteFilter(this)
filters.await().remove(this)
}
private fun spiltTag(tag: String) = tag.run {
@@ -124,30 +47,16 @@ object EhFilter {
}
}
@Synchronized
fun filterTag(info: GalleryInfo): Boolean {
val tags = info.simpleTags ?: return false
return tags.any { tag -> tagFilterList.any { it.enable!! && matchTag(tag, it.text!!) } }
}
private fun matchTagNamespace(tag: String, filter: String): Boolean {
val (nameSpace, _) = spiltTag(tag)
return nameSpace == filter
}
@Synchronized
fun filterTagNamespace(info: GalleryInfo): Boolean {
val tags = info.simpleTags ?: return false
return tags.any { tag -> tagNamespaceFilterList.any { it.enable!! && matchTagNamespace(tag, it.text!!) } }
}
@Synchronized
fun filterCommenter(commenter: String): Boolean {
return commenterFilterList.any { it.enable!! && it.text == commenter }
}
@Synchronized
fun filterComment(comment: String): Boolean {
return commentFilterList.any { it.enable!! && regex(it).containsMatchIn(comment) }
}
suspend fun needTags() = filters.await().any { it.enable && (it.mode == FilterMode.TAG || it.mode == FilterMode.TAG_NAMESPACE) }
suspend fun filterTitle(info: GalleryInfo) = anyActive(FilterMode.TITLE) { info.title.orEmpty().contains(it.text, true) }
suspend fun filterUploader(info: GalleryInfo) = anyActive(FilterMode.UPLOADER) { it.text == info.uploader }
suspend fun filterTag(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG) { matchTag(tag, it.text.lowercase()) } } ?: false
suspend fun filterTagNamespace(info: GalleryInfo) = info.simpleTags?.any { tag -> anyActive(FilterMode.TAG_NAMESPACE) { matchTagNamespace(tag, it.text.lowercase()) } } ?: false
suspend fun filterCommenter(commenter: String) = anyActive(FilterMode.COMMENTER) { it.text == commenter }
suspend fun filterComment(comment: String) = anyActive(FilterMode.COMMENT) { regex(it).containsMatchIn(comment) }
}
@@ -15,8 +15,20 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient
import android.util.Log
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArrayBuilder
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -25,10 +37,11 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.executeAsync
import org.json.JSONArray
import org.json.JSONObject
import okio.BufferedSource
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
addHeader("User-Agent", CHROME_USER_AGENT)
@@ -44,11 +57,54 @@ inline fun Request.Builder.multipartBody(builder: MultipartBody.Builder.() -> Un
val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
inline fun JSONObject.array(name: String, builder: JSONArray.() -> Unit): JSONObject = put(name, JSONArray().apply(builder))
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
fun jsonArrayOf(vararg element: Any?) = JSONArray().apply { element.forEach { put(it) } } // Should ensure it is inlined
inline fun Request.Builder.jsonBody(builder: JsonObjectBuilder.() -> Unit) = post(buildJsonObject(builder).toString().toRequestBody(MEDIA_TYPE_JSON))
inline fun Request.Builder.jsonBody(builder: JSONObject.() -> Unit) = post(JSONObject().apply(builder).toString().toRequestBody(MEDIA_TYPE_JSON))
const val TAG_CALL = "CancellableCallReadingScope"
// This scoped response reading scope suspend function aimed at cancelling the call immediately once coroutine is cancelled.
// i.e., bind a call with a suspended coroutine, and launch another coroutine to execute reading
// Once origin coroutine is cancelled, then call cancelled, socket is closed, the reading coroutine will encounter [IOException]
// Since Okio is not suspendable/cancellable, also okhttp, [executeAsync] only make [Request -> Response] cancellable, reading a Response is still not cancellable
// Considering interrupting is not safe and performant, See https://github.com/Kotlin/kotlinx.coroutines/issues/3551#issuecomment-1353245978, we have such [usingCancellable]
// TODO: Remove logging after this function stable
suspend inline fun <R> Call.usingCancellable(crossinline block: suspend Response.() -> R): R = executeAsync().use {
val call = this
Log.d(TAG_CALL, "Got response of call$call")
coroutineScope {
suspendCancellableCoroutine<R> { cont ->
launch {
val r = runCatching {
Log.d(TAG_CALL, "Reading response of call$call")
block(it)
}
if (!isCanceled() && cont.isActive) {
r.exceptionOrNull()?.let {
Log.e(TAG_CALL, "Reading response of call$call failed!")
cont.resumeWithException(it)
}
r.getOrNull()?.let {
Log.d(TAG_CALL, "Reading response of call$call succeed!")
cont.resume(it)
}
} else if (isCanceled() && cont.isActive) {
Log.e(TAG_CALL, "call$call cancelled but coroutine is Active!")
} else if (!isCanceled() && !cont.isActive) {
Log.e(TAG_CALL, "call$call not cancelled but coroutine is Dead!")
} else {
Log.d(TAG_CALL, "Reading response of call$call cancelled")
}
}
cont.invokeOnCancellation {
Log.d(TAG_CALL, "Cancelling call$call")
cancel()
}
}
}.apply {
Log.d(TAG_CALL, "Reading response of call$call finished!")
}
}
suspend inline fun <R> Request.execute(block: Response.() -> R): R {
contract {
@@ -56,14 +112,17 @@ suspend inline fun <R> Request.execute(block: Response.() -> R): R {
}
return okHttpClient.newCall(this).executeAsync().use(block)
}
suspend inline fun <R> Request.executeNonCache(crossinline block: suspend Response.() -> R) = baseOkHttpClient.newCall(this).usingCancellable(block)
suspend inline fun <R> Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block)
suspend inline fun <R> Request.executeNonCache(block: Response.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return nonCacheOkHttpClient.newCall(this).executeAsync().use(block)
}
suspend inline fun <reified T> Request.executeAndParseAs() = execute { parseAs<T>() }
const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
inline fun <reified T> Response.parseAs(): T = body.source().parseAs()
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
val json = Json { ignoreUnknownKeys = true }
const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36"
const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
@@ -1,74 +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 java.net.InetAddress
import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
private const val EXCEPTIONAL_DOMAIN = "hath.network"
object 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.takeIf { EhDns.isInHosts(host) || EXCEPTIONAL_DOMAIN in host }
val socket = sslSocketFactory.createSocket(s, address ?: host, 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)
}
}
@@ -16,12 +16,14 @@
package com.hippo.ehviewer.client
import android.content.Context
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
import com.hippo.ehviewer.R
import com.hippo.ehviewer.yorozuya.FileUtils
import com.hippo.ehviewer.yorozuya.copyToFile
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyToFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -34,33 +36,29 @@ import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.executeAsync
import okio.BufferedSource
import okio.HashingSink.Companion.sha1
import okio.blackholeSink
import okio.buffer
import okio.source
import org.json.JSONObject
import splitties.init.appCtx
import java.io.File
import java.nio.charset.StandardCharsets
private typealias TagGroup = Map<String, String>
private typealias TagGroups = Map<String, TagGroup>
object EhTagDatabase : CoroutineScope {
private const val NAMESPACE_PREFIX = "n"
private lateinit var tagGroups: TagGroups
private val PREFIXES = arrayOf(
"-",
"~",
"tag:",
"weak:",
)
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
private val dbLock = Mutex()
override val coroutineContext = Dispatchers.IO + Job()
fun isInitialized(): Boolean {
return this::tagGroups.isInitialized
}
private fun JSONObject.toTagGroups(): TagGroups =
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
private fun JSONObject.toTagGroup(): TagGroup =
keys().asSequence().associateWith { getString(it) }
var initialized by mutableStateOf(false)
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
@@ -101,22 +99,25 @@ object EhTagDatabase : CoroutineScope {
translate: Boolean,
exactly: Boolean = false,
): Flow<Pair<String?, String>> = flow {
val keywordPrefix = keyword.substringBefore(':')
val keywordTag = keyword.drop(keywordPrefix.length + 1)
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
var mKeyword = keyword
PREFIXES.forEach { mKeyword = mKeyword.removePrefix(it) }
val prefix = keyword.dropLast(mKeyword.length)
val namespace = mKeyword.substringBefore(':')
val tagKeyword = mKeyword.drop(namespace.length + 1)
val namespacePrefix = namespaceToPrefix(namespace) ?: namespace
val tags = tagGroups[namespacePrefix.takeIf { tagKeyword.isNotEmpty() && it != NAMESPACE_PREFIX }]
tags?.let {
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
internalSuggestFlow(it, tagKeyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix$namespacePrefix:$tag"))
}
} ?: tagGroups.forEach { (prefix, tags) ->
if (prefix != NAMESPACE_PREFIX) {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
} ?: tagGroups.forEach { (namespacePrefix, tags) ->
if (namespacePrefix != NAMESPACE_PREFIX) {
internalSuggestFlow(tags, mKeyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix$namespacePrefix:$tag"))
}
} else {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$tag:"))
internalSuggestFlow(tags, mKeyword, translate, exactly).collect { (hint, namespacePrefix) ->
emit(Pair(hint, "$prefix$namespacePrefix:"))
}
}
}
@@ -141,7 +142,6 @@ object EhTagDatabase : CoroutineScope {
it["reclass"] = "r"
}
@JvmStatic
fun namespaceToPrefix(namespace: String): String? {
return NAMESPACE_TO_PREFIX[namespace]
}
@@ -204,12 +204,13 @@ object EhTagDatabase : CoroutineScope {
private suspend fun issueUpdateInMemoryData() {
dbLock.withLock {
getMetadata(EhApplication.application)?.let { urls ->
getMetadata(appCtx)?.let { urls ->
val dataName = urls[2]
val dir = AppConfig.getFilesDir("tag-translations")
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
runSuspendCatching {
tagGroups = JSONObject(dataFile.source().buffer().readString(StandardCharsets.UTF_8)).toTagGroups()
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}.onFailure {
it.printStackTrace()
}
@@ -224,7 +225,7 @@ object EhTagDatabase : CoroutineScope {
}
private suspend fun updateInternal() {
val urls = getMetadata(EhApplication.application)
val urls = getMetadata(appCtx)
urls?.let {
val sha1Name = urls[0]
val sha1Url = urls[1]
@@ -244,11 +245,9 @@ object EhTagDatabase : CoroutineScope {
FileUtils.delete(dataFile)
}
val client = nonCacheOkHttpClient
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
check(save(client, sha1Url, tempSha1File))
check(save(baseOkHttpClient, sha1Url, tempSha1File))
val tempSha1 = getFileContent(tempSha1File)
// Check new sha1 and current sha1
@@ -260,7 +259,7 @@ object EhTagDatabase : CoroutineScope {
// Save new data
val tempDataFile = File(dir, "$dataName.tmp")
check(save(client, dataUrl, tempDataFile))
check(save(baseOkHttpClient, dataUrl, tempDataFile))
// Check new sha1 and new data
if (!checkData(tempSha1, tempDataFile)) {
@@ -16,26 +16,23 @@
package com.hippo.ehviewer.client
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.network.UrlBuilder
import okhttp3.HttpUrl
object EhUrl {
const val SITE_E = 0
const val SITE_EX = 1
const val DOMAIN_EX = "exhentai.org"
const val DOMAIN_E = "e-hentai.org"
const val DOMAIN_LOFI = "lofi.e-hentai.org"
const val HOST_EX = "https://$DOMAIN_EX/"
const val API_EX = HOST_EX + "api.php"
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
const val FAV_PATH = "favorites.php"
const val WATCHED_PATH = "watched"
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
const val URL_WATCHED_EX = HOST_EX + "watched"
const val HOST_E = "https://$DOMAIN_E/"
const val API_E = HOST_E + "api.php"
const val URL_FAVORITES_E = HOST_E + "favorites.php"
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
const val URL_MY_TAGS_E = HOST_E + "mytags"
const val URL_WATCHED_E = HOST_E + "watched"
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
const val URL_POPULAR_E = "https://e-hentai.org/popular"
const val URL_POPULAR_EX = "https://exhentai.org/popular"
@@ -59,11 +56,11 @@ object EhUrl {
else -> HOST_E
}
val favoritesUrl: String
val domain: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_FAVORITES_E
SITE_EX -> URL_FAVORITES_EX
else -> URL_FAVORITES_E
SITE_E -> DOMAIN_E
SITE_EX -> DOMAIN_EX
else -> HOST_E
}
val apiUrl: String
@@ -73,7 +70,6 @@ object EhUrl {
else -> API_E
}
@JvmStatic
val referer: String
get() = when (Settings.gallerySite) {
SITE_E -> REFERER_E
@@ -88,7 +84,6 @@ object EhUrl {
else -> ORIGIN_E
}
@JvmStatic
val uConfigUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_UCONFIG_E
@@ -96,7 +91,6 @@ object EhUrl {
else -> URL_UCONFIG_E
}
@JvmStatic
val myTagsUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_MY_TAGS_E
@@ -118,36 +112,20 @@ object EhUrl {
else -> URL_IMAGE_SEARCH_E
}
val watchedUrl: String
get() = when (Settings.gallerySite) {
SITE_E -> URL_WATCHED_E
SITE_EX -> URL_WATCHED_EX
else -> URL_WATCHED_E
}
fun getGalleryDetailUrl(gid: Long, token: String?): String {
return getGalleryDetailUrl(gid, token, 0, false)
}
@JvmStatic
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
if (index != 0) {
builder.addQuery("p", index)
}
if (allComment) {
builder.addQuery("hc", 1)
}
return builder.build()
}
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean) = ehUrl {
addPathSegments("g/$gid/$token/")
if (index != 0) addQueryParameter("p", "$index")
if (allComment) addQueryParameter("hc", "1")
}.toString()
@JvmStatic
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
return builder.build()
}
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl {
addPathSegments("mpv/$gid/$token/")
}.toString()
@JvmStatic
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
}
@@ -164,3 +142,9 @@ object EhUrl {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
}
inline fun httpsUrl(builder: HttpUrl.Builder.() -> Unit) = HttpUrl.Builder().apply(builder).scheme("https").build()
inline fun ehUrl(builder: HttpUrl.Builder.() -> Unit) = httpsUrl { apply(builder).host(EhUrl.domain) }
fun HttpUrl.Builder.addQueryParameterIfNotBlank(name: String, value: String?) = apply {
if (!value.isNullOrBlank()) addQueryParameter(name, value)
}
@@ -15,7 +15,15 @@
*/
package com.hippo.ehviewer.client
import android.text.TextUtils
import android.annotation.SuppressLint
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import com.google.android.material.color.utilities.Hct
import com.google.android.material.color.utilities.MathUtils
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryInfo
import java.util.regex.Pattern
@@ -79,7 +87,6 @@ object EhUtils {
val isExHentai: Boolean
get() = Settings.gallerySite == EhUrl.SITE_EX
@JvmStatic
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first)
@@ -94,8 +101,19 @@ object EhUtils {
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
}
fun getCategoryColor(category: Int): Int {
return when (category) {
@SuppressLint("RestrictedApi")
fun harmonizeWithRole(context: Context, src: Int): Int {
val primary = MaterialColors.getColor(context, R.attr.colorPrimaryContainer, "EhUtils")
val fromHct = Hct.fromInt(src)
val toHct = Hct.fromInt(primary)
val differenceDegrees = MathUtils.differenceDegrees(fromHct.hue, toHct.hue)
val rotationDegrees = minOf(differenceDegrees * 0.5, 15.0)
val outputHue = MathUtils.sanitizeDegreesDouble(fromHct.hue + rotationDegrees * MathUtils.rotationDirection(fromHct.hue, toHct.hue))
return Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
}
private fun getCategoryColor(context: Context, category: Int): Int {
val primary = when (category) {
DOUJINSHI -> BG_COLOR_DOUJINSHI
MANGA -> BG_COLOR_MANGA
ARTIST_CG -> BG_COLOR_ARTIST_CG
@@ -108,27 +126,29 @@ object EhUtils {
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
return harmonizeWithRole(context, primary)
}
@Composable
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
fun signOut() {
EhCookieStore.signOut()
Settings.putAvatar(null)
Settings.putDisplayName(null)
Settings.putGallerySite(EhUrl.SITE_E)
Settings.putNeedSignIn(true)
Settings.putSelectSite(true)
Settings.avatar = null
Settings.displayName = null
Settings.gallerySite = EhUrl.SITE_E
Settings.needSignIn = true
}
fun needSignedIn(): Boolean {
return Settings.needSignIn
}
@JvmStatic
fun getSuitableTitle(gi: GalleryInfo): String {
return if (Settings.showJpnTitle) {
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
} else {
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
}.orEmpty()
}
@@ -146,7 +166,6 @@ object EhUtils {
return title.ifEmpty { null }
}
@JvmStatic
fun handleThumbUrlResolution(url: String?): String? {
if (null == url) {
return null
@@ -0,0 +1,14 @@
package com.hippo.ehviewer.client.data
interface AbstractDownloadInfo {
var state: Int
var legacy: Int
var time: Long
var label: String?
var position: Int
var speed: Long
var remaining: Long
var finished: Int
var downloaded: Int
var total: Int
}
@@ -1,25 +0,0 @@
package com.hippo.ehviewer.client.data
interface AbstractGalleryInfo {
var gid: Long
var token: String?
var title: String?
var titleJpn: String?
var thumbKey: String?
var category: Int
var posted: String?
var uploader: String?
var disowned: Boolean
var rating: Float
var rated: Boolean
var simpleTags: Array<String>?
var pages: Int
var thumbWidth: Int
var thumbHeight: Int
var spanSize: Int
var spanIndex: Int
var spanGroupIndex: Int
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
}
@@ -15,12 +15,16 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "GALLERIES")
open class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
@@ -57,7 +61,7 @@ open class BaseGalleryInfo(
override var rated: Boolean = false,
@Ignore
override var simpleTags: Array<String>? = null,
override var simpleTags: ArrayList<String>? = null,
@Ignore
override var pages: Int = 0,
@@ -68,22 +72,17 @@ open class BaseGalleryInfo(
@Ignore
override var thumbHeight: Int = 0,
@Ignore
override var spanSize: Int = 0,
@Ignore
override var spanIndex: Int = 0,
@Ignore
override var spanGroupIndex: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@Ignore
override var favoriteSlot: Int = -2,
@ColumnInfo(name = "FAVORITE_SLOT")
override var favoriteSlot: Int = NOT_FAVORITED,
@Ignore
override var favoriteName: String? = null,
) : GalleryInfo
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo, Parcelable {
constructor() : this(0)
}
@@ -17,14 +17,14 @@ package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.network.UrlBuilder
import com.hippo.ehviewer.util.encodeUTF8
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl
import kotlinx.parcelize.Parcelize
@Parcelize
class FavListUrlBuilder(
private var mPrev: String? = null,
private var mNext: String? = null,
var mPrev: String? = null,
var mNext: String? = null,
var jumpTo: String? = null,
var keyword: String? = null,
var favCat: Int = FAV_CAT_ALL,
@@ -34,19 +34,18 @@ class FavListUrlBuilder(
mPrev = index.takeUnless { isNext }
}
fun build(): String {
val ub = UrlBuilder(EhUrl.favoritesUrl)
fun build() = ehUrl {
addPathSegment(EhUrl.FAV_PATH)
if (isValidFavCat(favCat)) {
ub.addQuery("favcat", favCat.toString())
addQueryParameter("favcat", "$favCat")
} else if (favCat == FAV_CAT_ALL) {
ub.addQuery("favcat", "all")
addQueryParameter("favcat", "all")
}
keyword?.takeIf { it.isNotBlank() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
mPrev?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("prev", it) }
mNext?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("next", it) }
jumpTo?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("seek", it) }
return ub.build()
}
addQueryParameterIfNotBlank("f_search", keyword)
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", jumpTo)
}.toString()
companion object {
const val FAV_CAT_ALL = -1
@@ -21,30 +21,17 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryComment(
// 0 for uploader comment. can't vote
@JvmField
var id: Long = 0,
@JvmField
var score: Int = 0,
@JvmField
var editable: Boolean = false,
@JvmField
var voteUpAble: Boolean = false,
@JvmField
var voteUpEd: Boolean = false,
@JvmField
var voteDownAble: Boolean = false,
@JvmField
var voteDownEd: Boolean = false,
@JvmField
var uploader: Boolean = false,
@JvmField
var voteState: String? = null,
@JvmField
var time: Long = 0,
@JvmField
var user: String? = null,
@JvmField
var comment: String? = null,
@JvmField
var lastEdited: Long = 0,
) : Parcelable
@@ -15,6 +15,7 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
import kotlinx.parcelize.Parcelize
@@ -37,25 +38,24 @@ private val LANGUAGES = arrayOf(
@Parcelize
class GalleryDetail(
val galleryInfo: GalleryInfo = BaseGalleryInfo(),
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
var apiKey: String? = null,
var torrentCount: Int = 0,
var torrentUrl: String? = null,
var archiveUrl: String? = null,
var parent: String? = null,
var newerVersions: ArrayList<GalleryInfo> = ArrayList(),
var newerVersions: ArrayList<BaseGalleryInfo> = ArrayList(),
var visible: String? = null,
var language: String? = null,
var size: String? = null,
var favoriteCount: Int = 0,
var isFavorited: Boolean = false,
var ratingCount: Int = 0,
val tags: Array<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewPages: Int,
val previewList: List<GalleryPreview>,
) : AbstractGalleryInfo by galleryInfo, GalleryInfo {
) : GalleryInfo by galleryInfo, Parcelable {
override fun generateSLang() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = S_LANGS[index]
@@ -15,16 +15,35 @@
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import java.util.regex.Pattern
interface GalleryInfo : AbstractGalleryInfo, Parcelable {
interface GalleryInfo {
var gid: Long
var token: String?
var title: String?
var titleJpn: String?
var thumbKey: String?
var category: Int
var posted: String?
var uploader: String?
var disowned: Boolean
var rating: Float
var rated: Boolean
var simpleTags: ArrayList<String>?
var pages: Int
var thumbWidth: Int
var thumbHeight: Int
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
var favoriteNote: String?
fun generateSLang() {
simpleLanguage = simpleTags?.let { generateSLangFromTags(it) }
?: title?.let { generateSLangFromTitle(it) }
}
private fun generateSLangFromTags(simpleTags: Array<String>): String? {
private fun generateSLangFromTags(simpleTags: List<String>): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
@@ -127,5 +146,8 @@ interface GalleryInfo : AbstractGalleryInfo, Parcelable {
"language:hungarian",
"language:dutch",
)
const val NOT_FAVORITED = -2
const val LOCAL_FAVORITED = -1
}
}
@@ -20,7 +20,6 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryTagGroup(
private val mTagList: ArrayList<String> = arrayListOf(),
@JvmField
var groupName: String? = null,
val groupName: String,
private val mTagList: MutableList<String> = mutableListOf(),
) : Parcelable, MutableList<String> by mTagList
@@ -16,16 +16,15 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.network.UrlBuilder
import com.hippo.ehviewer.ui.legacy.AdvanceSearchTable
import com.hippo.ehviewer.ui.main.AdvanceTable
import com.hippo.ehviewer.util.encodeUTF8
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.yorozuya.StringUtils
import com.hippo.ehviewer.util.toIntOrDefault
import kotlinx.parcelize.Parcelize
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
@@ -35,11 +34,11 @@ data class ListUrlBuilder(
@get:Mode @Mode
var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
private var mNext: String? = null,
private var mJumpTo: String? = null,
var mNext: String? = null,
var mJumpTo: String? = null,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
private var mSHash: String? = null,
var hash: String? = null,
var advanceSearch: Int = -1,
var minRating: Int = -1,
var pageFrom: Int = -1,
@@ -63,7 +62,7 @@ data class ListUrlBuilder(
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
mSHash = null
hash = null
}
fun setIndex(index: String?, isNext: Boolean = true) {
@@ -71,8 +70,8 @@ data class ListUrlBuilder(
mPrev = index?.takeUnless { isNext }
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
fun setJumpTo(jumpTo: Int) {
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
}
var keyword: String?
@@ -82,6 +81,7 @@ data class ListUrlBuilder(
}
fun set(q: QuickSearch) {
reset()
mode = q.mode
this.category = q.category
mKeyword = q.keyword
@@ -89,21 +89,20 @@ data class ListUrlBuilder(
minRating = q.minRating
pageFrom = q.pageFrom
pageTo = q.pageTo
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
mNext = q.name.substringAfterLast('@', "").takeIf { it.isNotEmpty() }
}
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 toQuickSearch(name: String): QuickSearch {
return QuickSearch(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
pageTo = pageTo,
)
}
fun equalsQuickSearch(q: QuickSearch?): Boolean {
@@ -116,7 +115,7 @@ data class ListUrlBuilder(
if (q.category != this.category) {
return false
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
if (q.keyword != mKeyword) {
return false
}
if (q.advanceSearch != advanceSearch) {
@@ -138,10 +137,10 @@ data class ListUrlBuilder(
// TODO page
fun setQuery(query: String?) {
reset()
if (TextUtils.isEmpty(query)) {
if (query.isNullOrEmpty()) {
return
}
val queries = StringUtils.split(query, '&')
val queries = query.split('&')
var category = 0
var keyword: String? = null
var enableAdvanceSearch = false
@@ -160,7 +159,7 @@ data class ListUrlBuilder(
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhUtils.ALL_CATEGORY)
val cats = value.toIntOrDefault(EhUtils.ALL_CATEGORY)
category = category or (cats.inv() and EhUtils.ALL_CATEGORY)
}
@@ -216,37 +215,37 @@ data class ListUrlBuilder(
}
"f_sh" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SH
advanceSearch = advanceSearch or AdvanceTable.SH
}
"f_sto" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.STO
advanceSearch = advanceSearch or AdvanceTable.STO
}
"f_sfl" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFL
advanceSearch = advanceSearch or AdvanceTable.SFL
}
"f_sfu" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFU
advanceSearch = advanceSearch or AdvanceTable.SFU
}
"f_sft" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFT
advanceSearch = advanceSearch or AdvanceTable.SFT
}
"f_sr" -> if ("on" == value) {
enableMinRating = true
}
"f_srdd" -> minRating = NumberUtils.parseIntSafely(value, -1)
"f_srdd" -> minRating = value.toIntOrDefault(-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
"f_spf" -> pageFrom = value.toIntOrDefault(-1)
"f_spt" -> pageTo = value.toIntOrDefault(-1)
"f_shash" -> hash = value
}
}
this.category = category
@@ -272,63 +271,66 @@ data class ListUrlBuilder(
fun build(): String {
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.host
} else {
EhUrl.watchedUrl
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhUtils.ALL_CATEGORY)
}
mSHash?.let {
ub.addQuery("f_shash", it)
}
mJumpTo?.let {
ub.addQuery("seek", it)
}
mPrev?.let {
ub.addQuery("prev", it)
}
mNext?.let {
ub.addQuery("next", it)
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl {
if (mode == MODE_SUBSCRIPTION) addPathSegment(EhUrl.WATCHED_PATH)
if (category != EhUtils.NONE) {
addEncodedQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
}
addQueryParameterIfNotBlank("f_shash", hash)
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", mJumpTo)
// Search key
// the settings of ub:UrlBuilder may be overwritten by following Advance search
StringUtils.split(mKeyword, '|')?.forEachIndexed { idx, keyword ->
val keyword = keyword.trim { it <= ' ' }
mKeyword?.split('|')?.forEachIndexed { idx, kwd ->
val keyword = kwd.trim { it <= ' ' }
when (idx) {
0 -> keyword.takeIf { it.isNotEmpty() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
0 -> addQueryParameterIfNotBlank("f_search", keyword)
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
val key = keyword.substring(0, this).trim { it <= ' ' }
val value = keyword.substring(this + 1).trim { it <= ' ' }
ub.addQuery(key, encodeUTF8(value))
addQueryParameterIfNotBlank(key, value)
}
}
}
// 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")
addQueryParameter("advsearch", "1")
if (advanceSearch and AdvanceTable.SH != 0) {
addQueryParameter("f_sh", "on")
}
if (advanceSearch and AdvanceTable.STO != 0) {
addQueryParameter("f_sto", "on")
}
if (advanceSearch and AdvanceTable.SFL != 0) {
addQueryParameter("f_sfl", "on")
}
if (advanceSearch and AdvanceTable.SFU != 0) {
addQueryParameter("f_sfu", "on")
}
if (advanceSearch and AdvanceTable.SFT != 0) {
addQueryParameter("f_sft", "on")
}
// Set min star
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
addQueryParameter("f_sr", "on")
addQueryParameter("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 "")
addQueryParameter("f_sp", "on")
addQueryParameter(
"f_spf",
(if (pageFrom != -1) pageFrom.toString() else "").toString(),
)
addQueryParameter(
"f_spt",
(if (pageTo != -1) pageTo.toString() else "").toString(),
)
}
}
ub.build()
}
}.toString()
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.host)
@@ -0,0 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)
@@ -15,7 +15,11 @@
*/
package com.hippo.ehviewer.client.exception
import androidx.annotation.StringRes
import splitties.init.appCtx
open class EhException : Exception {
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
constructor(@StringRes message: Int) : super(appCtx.getString(message))
}
@@ -0,0 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class InsufficientFundsException : EhException(R.string.insufficient_funds)
@@ -0,0 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class NotLoggedInException : EhException(R.string.sign_in_first)
@@ -16,6 +16,6 @@
package com.hippo.ehviewer.client.exception
/**
* It is an exception for get offensive tip for g.e-hentai.org
* It is an exception for get offensive tip for e-hentai.org
*/
class OffensiveException : EhException("OFFENSIVE")
@@ -15,6 +15,7 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import org.jsoup.Jsoup
@@ -27,8 +28,10 @@ object ArchiveParser {
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
private val PATTERN_HATH_ARCHIVE =
Regex("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
private val PATTERN_NEED_HATH_CLIENT =
Regex("You must have a H@H client assigned to your account to use this feature\\.")
private const val ERROR_NEED_HATH_CLIENT =
"You must have a H@H client assigned to your account to use this feature."
private const val ERROR_INSUFFICIENT_FUNDS =
"You do not have enough funds to download this archive."
fun parse(body: String): Result {
val m = PATTERN_HATH_FORM.find(body)!!
@@ -65,9 +68,12 @@ object ArchiveParser {
return result
}
@Throws(NoHAtHClientException::class)
fun parseArchiveUrl(body: String): String? {
if (PATTERN_NEED_HATH_CLIENT.containsMatchIn(body)) throw NoHAtHClientException("No H@H client")
if (body.contains(ERROR_NEED_HATH_CLIENT)) {
throw NoHAtHClientException("No H@H client")
} else if (body.contains(ERROR_INSUFFICIENT_FUNDS)) {
throw InsufficientFundsException()
}
return Jsoup.parse(body).selectFirst("#continue>a[href]")
?.let { it.attr("href") + "?start=1" }
// TODO: Check more errors
@@ -0,0 +1,6 @@
package com.hippo.ehviewer.client.parser
import kotlinx.serialization.Serializable
@Serializable
data class Error(val error: String)
@@ -1,62 +1,31 @@
/*
* 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.client.parser
import com.hippo.ehviewer.GetText.getString
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import org.jsoup.Jsoup
import splitties.init.appCtx
object FavoritesParser {
fun parse(body: String): Result {
suspend fun parse(body: String): Result {
if (body.contains("This page requires you to log on.</p>")) {
throw EhException(getString(R.string.need_sign_in))
throw EhException(appCtx.getString(R.string.need_sign_in))
}
val catArray = arrayOfNulls<String>(10)
val countArray = IntArray(10)
val d = Jsoup.parse(body)
runCatching {
val ido = JsoupUtils.getElementByClass(d, "ido")
val fps = ido!!.getElementsByClass("fp")
// Last one is "fp fps"
check(fps.size == 11)
for (i in 0..9) {
val fp = fps[i]
countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0)
catArray[i] = ParserUtils.trim(fp.child(2).text())
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
throw ParseException("Parse favorites error", body)
}
val result = GalleryListParser.parse(d, body)
return Result(catArray, countArray, result)
val countArray = parseFav(body, catArray)
if (countArray.isEmpty()) throw ParseException("Parse favorites error", body)
val result = GalleryListParser.parse(body)
return Result(catArray.requireNoNulls(), countArray, result)
}
class Result(
val catArray: Array<String?>,
val catArray: Array<String>,
val countArray: IntArray,
galleryListResult: GalleryListParser.Result,
galleryListResult: GalleryListResult,
) {
val prev = galleryListResult.prev
val next = galleryListResult.next
val galleryInfoList = galleryListResult.galleryInfoList
}
}
private external fun parseFav(body: String, favCat: Array<String?>): IntArray
@@ -16,7 +16,7 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.util.ExceptionUtils
import org.jsoup.Jsoup
@@ -29,7 +29,7 @@ object ForumsParser {
child.attr("href")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException("Not logged in")
throw NotLoggedInException()
}
}
}
@@ -19,30 +19,46 @@ import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.yorozuya.NumberUtils
import org.json.JSONObject
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryApiParser {
fun parse(body: String, galleryInfoList: List<GalleryInfo>) {
val jo = JSONObject(body)
val ja = jo.getJSONArray("gmetadata")
for (i in 0 until ja.length()) {
val g = ja.getJSONObject(i)
val gid = g.getLong("gid")
val gi = galleryInfoList.find { it.gid == gid } ?: continue
gi.title = ParserUtils.trim(g.getString("title"))
gi.titleJpn = ParserUtils.trim(g.getString("title_jpn"))
gi.category = getCategory(g.getString("category"))
gi.thumbKey = getThumbKey(handleThumbUrlResolution(g.getString("thumb"))!!)
gi.uploader = g.getString("uploader")
gi.posted =
ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted"), 0) * 1000)
gi.rating = NumberUtils.parseFloatSafely(g.getString("rating"), 0.0f)
// tags
val tagJa = g.getJSONArray("tags")
gi.simpleTags = (0 until tagJa.length()).map { tagJa.getString(it) }.toTypedArray()
gi.pages = NumberUtils.parseIntSafely(g.getString("filecount"), 0)
gi.generateSLang()
body.parseAs<Result>().items.forEach { item ->
val gi = galleryInfoList.find { it.gid == item.gid } ?: return@forEach
gi.apply {
title = item.title.unescapeXml()
titleJpn = item.titleJpn.unescapeXml()
category = getCategory(item.category)
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
uploader = item.uploader.unescapeXml()
posted = ParserUtils.formatDate(item.posted * 1000)
rating = item.rating
simpleTags = item.tags
pages = item.pages
generateSLang()
}
}
}
@Serializable
data class Result(@SerialName("gmetadata") val items: List<Item>)
@Serializable
data class Item(
val gid: Long,
val title: String,
@SerialName("title_jpn")
val titleJpn: String,
val category: String,
val thumb: String,
val uploader: String,
val posted: Long,
@SerialName("filecount")
val pages: Int,
val rating: Float,
val tags: ArrayList<String>,
)
}
@@ -25,6 +25,8 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryComment
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.LargeGalleryPreview
@@ -35,11 +37,10 @@ import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.exception.PiningException
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.yorozuya.StringUtils
import com.hippo.ehviewer.yorozuya.trimAnd
import com.hippo.ehviewer.yorozuya.unescapeXml
import com.hippo.ehviewer.util.toFloatOrDefault
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.trimAnd
import com.hippo.ehviewer.util.unescapeXml
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -82,8 +83,7 @@ object GalleryDetailParser {
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
@Throws(EhException::class)
fun parse(body: String): GalleryDetail {
suspend fun parse(body: String): GalleryDetail {
if (body.contains(OFFENSIVE_STRING)) {
throw OffensiveException()
}
@@ -108,8 +108,7 @@ object GalleryDetailParser {
return galleryDetail
}
@Throws(ParseException::class)
private fun parseDetail(gd: GalleryDetail, d: Document, body: String) {
private suspend fun parseDetail(gd: GalleryDetail, d: Document, body: String) {
PATTERN_DETAIL.find(body)?.apply {
gd.gid = groupValues[1].toLongOrNull() ?: -1L
gd.token = groupValues[2]
@@ -141,11 +140,11 @@ object GalleryDetailParser {
gd.titleJpn = gm.getElementById("gj")?.text()?.trim()
// Category
val gdc = gm.getElementById("gdc")
try {
var ce = JsoupUtils.getElementByClass(gdc, "cn")
val gdc = gm.getElementById("gdc")!!
var ce = gdc.getElementsByClass("cn").first()
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs")
ce = gdc.getElementsByClass("cs").first()
}
gd.category = getCategory(ce!!.text())
} catch (e: Throwable) {
@@ -157,7 +156,7 @@ object GalleryDetailParser {
val gdn = gm.getElementById("gdn")
if (null != gdn) {
gd.disowned = gdn.attr("style").contains("opacity:0.5")
gd.uploader = StringUtils.trim(gdn.text())
gd.uploader = gdn.text().trim()
} else {
gd.uploader = ""
}
@@ -176,10 +175,7 @@ object GalleryDetailParser {
// Rating count
val ratingCount = gm.getElementById("rating_count")
if (null != ratingCount) {
gd.ratingCount = NumberUtils.parseIntSafely(
StringUtils.trim(ratingCount.text()),
0,
)
gd.ratingCount = ratingCount.text().trim().toIntOrDefault(0)
} else {
gd.ratingCount = 0
}
@@ -187,7 +183,7 @@ object GalleryDetailParser {
// Rating
val ratingLabel = gm.getElementById("rating_label")
if (null != ratingLabel) {
val ratingStr = StringUtils.trim(ratingLabel.text())
val ratingStr = ratingLabel.text().trim()
if ("Not Yet Rated" == ratingStr) {
gd.rating = -1.0f
} else {
@@ -195,7 +191,7 @@ object GalleryDetailParser {
if (index == -1 || index >= ratingStr.length) {
gd.rating = 0f
} else {
gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f)
gd.rating = ratingStr.substring(index + 1).toFloatOrDefault(0f)
}
}
} else {
@@ -204,21 +200,17 @@ object GalleryDetailParser {
// isFavorited
val gdf = gm.getElementById("gdf")
gd.isFavorited = false
if (gdf != null) {
val favoriteName = StringUtils.trim(gdf.text())
if (favoriteName == "Add to Favorites") {
gd.favoriteName = null
} else {
gd.isFavorited = true
gd.favoriteName = StringUtils.trim(gdf.text())
val favoriteName = gdf.text().trim()
if (favoriteName != "Add to Favorites") {
gd.favoriteName = gdf.text().trim()
PATTERN_FAVORITE_SLOT.find(body)?.run {
gd.favoriteSlot = ((groupValues[1].toIntOrNull() ?: 2) - 2) / 19
}
}
}
if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {
gd.favoriteSlot = -1
if (gd.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(gd.gid)) {
gd.favoriteSlot = LOCAL_FAVORITED
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
@@ -234,7 +226,7 @@ object GalleryDetailParser {
if (result != null) {
gi.gid = result.gid
gi.token = result.token
gi.title = StringUtils.trim(element.text())
gi.title = element.text().trim()
gi.posted = dates[index]
gd.newerVersions.add(gi)
}
@@ -247,8 +239,8 @@ object GalleryDetailParser {
if (es.size < 2) {
return
}
val key = StringUtils.trim(es[0].text())
val value = StringUtils.trim(es[1].ownText())
val key = es[0].text().trim()
val value = es[1].ownText().trim()
if (key.startsWith("Posted")) {
gd.posted = value
} else if (key.startsWith("Parent")) {
@@ -265,7 +257,7 @@ object GalleryDetailParser {
} else if (key.startsWith("Length")) {
val index = value.indexOf(' ')
if (index >= 0) {
gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1)
gd.pages = value.substring(0, index).toIntOrDefault(1)
} else {
gd.pages = 1
}
@@ -278,7 +270,7 @@ object GalleryDetailParser {
if (index == -1) {
gd.favoriteCount = 0
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0)
gd.favoriteCount = value.substring(0, index).toIntOrDefault(0)
}
}
}
@@ -287,11 +279,10 @@ object GalleryDetailParser {
private fun parseTagGroup(element: Element): GalleryTagGroup? {
return try {
val group = GalleryTagGroup()
var nameSpace = element.child(0).text()
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
group.groupName = nameSpace
val group = GalleryTagGroup(nameSpace)
val tags = element.child(1).children()
tags.forEach {
var tag = it.text()
@@ -338,7 +329,7 @@ object GalleryDetailParser {
}
}
private fun parseComment(element: Element): GalleryComment? {
private suspend fun parseComment(element: Element): GalleryComment? {
return try {
val comment = GalleryComment()
// Id
@@ -346,7 +337,7 @@ object GalleryDetailParser {
val name = a!!.attr("name")
comment.id = name trimAnd { substring(1).toInt().toLong() }
// Editable, vote up and vote down
val c4 = JsoupUtils.getElementByClass(element, "c4")
val c4 = element.getElementsByClass("c4").first()
if (null != c4) {
if ("Uploader Comment" == c4.text()) {
comment.uploader = true
@@ -355,12 +346,12 @@ object GalleryDetailParser {
when (e.text()) {
"Vote+" -> {
comment.voteUpAble = true
comment.voteUpEd = StringUtils.trim(e.attr("style")).isNotEmpty()
comment.voteUpEd = e.attr("style").trim().isNotEmpty()
}
"Vote-" -> {
comment.voteDownAble = true
comment.voteDownEd = StringUtils.trim(e.attr("style")).isNotEmpty()
comment.voteDownEd = e.attr("style").trim().isNotEmpty()
}
"Edit" -> comment.editable = true
@@ -368,23 +359,20 @@ object GalleryDetailParser {
}
}
// Vote state
val c7 = JsoupUtils.getElementByClass(element, "c7")
val c7 = element.getElementsByClass("c7").first()
if (null != c7) {
comment.voteState = StringUtils.trim(c7.text())
comment.voteState = c7.text().trim()
}
// Score
val c5 = JsoupUtils.getElementByClass(element, "c5")
val c5 = element.getElementsByClass("c5").first()
if (null != c5) {
val es = c5.children()
if (!es.isEmpty()) {
comment.score = NumberUtils.parseIntSafely(
StringUtils.trim(es[0].text()),
0,
)
comment.score = es[0].text().trim().toIntOrDefault(0)
}
}
// time
val c3 = JsoupUtils.getElementByClass(element, "c3")
val c3 = element.getElementsByClass("c3").first()
val temp = c3!!.ownText()
val time = if (temp.endsWith(':')) {
// user
@@ -395,7 +383,7 @@ object GalleryDetailParser {
}
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = JsoupUtils.getElementByClass(element, "c6")
val c6 = element.getElementsByClass("c6").first()
// fix underline support
for (e in c6!!.children()) {
if ("span" == e.tagName() && "text-decoration:underline;" == e.attr("style")) {
@@ -411,7 +399,7 @@ object GalleryDetailParser {
}
}
// last edited
val c8 = JsoupUtils.getElementByClass(element, "c8")
val c8 = element.getElementsByClass("c8").first()
if (c8 != null) {
val e = c8.children().first()
if (e != null) {
@@ -430,7 +418,7 @@ object GalleryDetailParser {
/**
* Parse comments with html parser
*/
fun parseComments(document: Document): GalleryCommentList {
suspend fun parseComments(document: Document): GalleryCommentList {
return try {
val cdiv = document.getElementById("cdiv")!!
val c1s = cdiv.getElementsByClass("c1")
@@ -460,7 +448,6 @@ object GalleryDetailParser {
/**
* Parse preview pages with regular expressions
*/
@Throws(ParseException::class)
fun parsePreviewPages(body: String): Int {
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error", body)
@@ -469,7 +456,6 @@ object GalleryDetailParser {
/**
* Parse pages with regular expressions
*/
@Throws(ParseException::class)
fun parsePages(body: String): Int {
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error", body)
@@ -16,7 +16,7 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.util.toLongOrDefault
import java.util.regex.Pattern
/**
@@ -24,7 +24,7 @@ import java.util.regex.Pattern
*/
object GalleryDetailUrlParser {
private val URL_STRICT_PATTERN = Pattern.compile(
"https?://(?:" + EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
)
private val URL_PATTERN = Pattern.compile("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
@@ -33,7 +33,7 @@ object GalleryDetailUrlParser {
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
val m = pattern.matcher(url)
return if (m.find()) {
val gid = NumberUtils.parseLongSafely(m.group(1), 0).takeIf { it > 0 }
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
gid?.let { Result(it, m.group(2)!!) }
} else {
null
@@ -1,343 +1,38 @@
/*
* 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.client.parser
import android.util.Log
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import com.hippo.ehviewer.yorozuya.NumberUtils
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.regex.Pattern
object GalleryListParser {
private val TAG = GalleryListParser::class.java.simpleName
private const val NO_UNFILTERED_TEXT =
"No unfiltered results in this page range. You either requested an invalid page or used too aggressive filters."
private val PATTERN_RATING = Pattern.compile("\\d+px")
private val PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px;width:(\\d+)px")
private val PATTERN_FAVORITE_SLOT =
Pattern.compile("background-color:rgba\\((\\d+),(\\d+),(\\d+),")
private val PATTERN_PAGES = Pattern.compile("(\\d+) page")
private val PATTERN_NEXT_PAGE = Pattern.compile("p=(\\d+)")
private val PATTERN_PREV = Pattern.compile("prev=(\\d+(-\\d+)?)")
private val PATTERN_NEXT = Pattern.compile("next=(\\d+(-\\d+)?)")
private val FAVORITE_SLOT_RGB = arrayOf(
Triple("0", "0", "0"),
Triple("240", "0", "0"),
Triple("240", "160", "0"),
Triple("208", "208", "0"),
Triple("0", "128", "0"),
Triple("144", "240", "64"),
Triple("64", "176", "240"),
Triple("0", "0", "240"),
Triple("80", "0", "128"),
Triple("224", "128", "224"),
)
private fun parseRating(ratingStyle: String): String? {
val m = PATTERN_RATING.matcher(ratingStyle)
var num1 = Int.MIN_VALUE
var num2 = Int.MIN_VALUE
var rate = 5
if (m.find()) {
num1 = ParserUtils.parseInt(m.group().replace("px", ""), Int.MIN_VALUE)
}
if (m.find()) {
num2 = ParserUtils.parseInt(m.group().replace("px", ""), Int.MIN_VALUE)
}
if (num1 == Int.MIN_VALUE || num2 == Int.MIN_VALUE) {
return null
}
rate -= num1 / 16
return if (num2 == 21) {
"${rate - 1}.5"
} else {
rate.toString()
}
}
private fun parseFavoriteSlot(style: String): Int {
val m = PATTERN_FAVORITE_SLOT.matcher(style)
if (m.find()) {
val r = m.group(1)!!
val g = m.group(2)!!
val b = m.group(3)!!
return FAVORITE_SLOT_RGB.indexOf(Triple(r, g, b))
}
return -2
}
private fun parseGalleryInfo(e: Element): GalleryInfo? {
val gi: GalleryInfo = BaseGalleryInfo()
// Title, gid, token (required), tags
val glname = JsoupUtils.getElementByClass(e, "glname")
if (glname != null) {
var a = JsoupUtils.getElementByTag(glname, "a")
if (a == null) {
val parent = glname.parent()
if (parent != null && "a" == parent.tagName()) {
a = parent
}
}
if (a != null) {
val result = GalleryDetailUrlParser.parse(a.attr("href"))
if (result != null) {
gi.gid = result.gid
gi.token = result.token
}
}
var child: Element = glname
var children = glname.children()
while (children.size != 0) {
child = children[0]
children = child.children()
}
gi.title = child.text().trim { it <= ' ' }
}
if (gi.title == null) {
return null
}
// Tags
val gts = e.select(".gt, .gtl")
if (gts.size != 0) {
val tags = ArrayList<String>()
for (gt in gts) {
tags.add(gt.attr("title"))
}
gi.simpleTags = tags.toTypedArray()
}
// Category
gi.category = EhUtils.UNKNOWN
var ce = JsoupUtils.getElementByClass(e, "cn")
if (ce == null) {
ce = JsoupUtils.getElementByClass(e, "cs")
}
if (ce != null) {
gi.category = EhUtils.getCategory(ce.text())
}
// Thumb
var thumbUrl: String? = null
val glthumb = JsoupUtils.getElementByClass(e, "glthumb")
if (glthumb != null) {
val img = glthumb.select("div:nth-child(1)>img").first()
if (img != null) {
// Thumb size
val m = PATTERN_THUMB_SIZE.matcher(img.attr("style"))
if (m.find()) {
gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0)
gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0)
} else {
Log.w(TAG, "Can't parse gallery info thumb size")
gi.thumbWidth = 0
gi.thumbHeight = 0
}
// Thumb url
var url = img.attr("data-src")
if (url.isEmpty()) {
url = img.attr("src")
}
if (url.isNotEmpty()) {
thumbUrl = EhUtils.handleThumbUrlResolution(url)!!
}
}
// Pages
val div = glthumb.select("div:nth-child(2)>div:nth-child(2)>div:nth-child(2)").first()
if (div != null) {
val matcher = PATTERN_PAGES.matcher(div.text())
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)
suspend fun parse(body: String) = runCatching {
parseGalleryInfoList(body).apply {
galleryInfoList.onEach {
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
it.favoriteSlot = LOCAL_FAVORITED
}
it.generateSLang()
}
}
// Try extended and thumbnail version
if (thumbUrl == null) {
var gl = JsoupUtils.getElementByClass(e, "gl1e")
if (gl == null) {
gl = JsoupUtils.getElementByClass(e, "gl3t")
}
if (gl != null) {
val img = JsoupUtils.getElementByTag(gl, "img")
if (img != null) {
// Thumb size
val m = PATTERN_THUMB_SIZE.matcher(img.attr("style"))
if (m.find()) {
gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0)
gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0)
} else {
Log.w(TAG, "Can't parse gallery info thumb size")
gi.thumbWidth = 0
gi.thumbHeight = 0
}
thumbUrl = EhUtils.handleThumbUrlResolution(img.attr("src"))
}
}
}.getOrElse {
if (body.contains("<p>You do not have any watched tags")) {
throw EhException(R.string.gallery_list_empty_hit_subscription)
}
gi.thumbKey = getThumbKey(thumbUrl!!)
// Posted
val posted = e.getElementById("posted_" + gi.gid)
if (posted != null) {
gi.posted = posted.text().trim { it <= ' ' }
gi.favoriteSlot = parseFavoriteSlot(posted.attr("style"))
if (body.contains("No hits found</p>")) {
throw EhException(R.string.gallery_list_empty_hit)
}
if (gi.favoriteSlot < 0) {
gi.favoriteSlot = if (EhDB.containLocalFavorites(gi.gid)) -1 else -2
}
// Rating
val ir = JsoupUtils.getElementByClass(e, "ir")
if (ir != null) {
gi.rating = NumberUtils.parseFloatSafely(parseRating(ir.attr("style")), -1.0f)
// TODO The gallery may be rated even if it doesn't has one of these classes
gi.rated = ir.hasClass("irr") || ir.hasClass("irg") || ir.hasClass("irb")
}
// Uploader and pages
var gl = JsoupUtils.getElementByClass(e, "glhide")
var uploaderIndex = 0
var pagesIndex = 1
if (gl == null) {
// For extended
gl = JsoupUtils.getElementByClass(e, "gl3e")
uploaderIndex = 3
pagesIndex = 4
}
if (gl != null) {
val children = gl.children()
if (children.size > uploaderIndex) {
val div = children[uploaderIndex]
if (div != null) {
gi.disowned = div.attr("style").contains("opacity:0.5")
val a = div.children().first()
gi.uploader = a?.text()?.trim { it <= ' ' } ?: div.text().trim { it <= ' ' }
}
}
if (children.size > pagesIndex) {
val matcher = PATTERN_PAGES.matcher(children[pagesIndex].text())
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)
}
}
}
// For thumbnail
val gl5t = JsoupUtils.getElementByClass(e, "gl5t")
if (gl5t != null) {
val div = gl5t.select("div:nth-child(2)>div:nth-child(2)").first()
if (div != null) {
val matcher = PATTERN_PAGES.matcher(div.text())
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)
}
}
}
gi.generateSLang()
return gi
}
fun parse(body: String): Result {
val d = Jsoup.parse(body)
return parse(d, body)
}
fun parse(d: Document, body: String): Result {
val result = Result()
try {
val prev = d.getElementById("uprev")
val next = d.getElementById("unext")
assert(prev != null)
assert(next != null)
val matcherPrev = PATTERN_PREV.matcher(prev!!.attr("href"))
val matcherNext = PATTERN_NEXT.matcher(next!!.attr("href"))
if (matcherPrev.find()) result.prev = matcherPrev.group(1)
if (matcherNext.find()) result.next = matcherNext.group(1)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
result.noWatchedTags = body.contains("<p>You do not have any watched tags")
if (body.contains("No hits found</p>")) {
return result
}
}
try { // For toplists
val ptt = d.getElementsByClass("ptt").first()
if (ptt != null) {
val es = ptt.child(0).child(0).children()
result.pages = es[es.size - 2].text().trim { it <= ' ' }.toInt()
var e = es[es.size - 1]
if (e != null) {
e = e.children().first()
if (e != null) {
val href = e.attr("href")
val matcher = PATTERN_NEXT_PAGE.matcher(href)
if (matcher.find()) {
result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0)
}
}
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
try {
val itg = d.getElementsByClass("itg").first()
val es = if ("table".equals(itg!!.tagName(), ignoreCase = true)) {
itg.child(0).children()
} else {
itg.children()
}
val list = result.galleryInfoList
// First one is table header, skip it
for (i in es.indices) {
val gi = parseGalleryInfo(es[i])
if (null != gi) {
list.add(gi)
}
}
if (list.isEmpty()) {
if (es.size < 2 || NO_UNFILTERED_TEXT != es[1].text()) {
Log.d(TAG, "No gallery found")
}
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
throw ParseException("Can't parse gallery list", body)
}
return result
}
class Result {
var pages = 0
var nextPage = 0
var prev: String? = null
var next: String? = null
var noWatchedTags = false
val galleryInfoList = mutableListOf<GalleryInfo>()
throw ParseException("Can't parse gallery list", body, it)
}
}
class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -17,10 +17,8 @@ package com.hippo.ehviewer.client.parser
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.yorozuya.Utilities
import java.io.UnsupportedEncodingException
import java.net.MalformedURLException
import java.net.URL
@@ -28,7 +26,7 @@ import java.net.URLDecoder
import java.nio.charset.StandardCharsets
object GalleryListUrlParser {
private val VALID_HOSTS = arrayOf(EhUrl.DOMAIN_EX, EhUrl.DOMAIN_E, EhUrl.DOMAIN_LOFI)
private val VALID_HOSTS = arrayOf(EhUrl.DOMAIN_EX, EhUrl.DOMAIN_E)
private const val PATH_NORMAL = "/"
private const val PATH_UPLOADER = "/uploader/"
private const val PATH_TAG = "/tag/"
@@ -39,7 +37,7 @@ object GalleryListUrlParser {
} catch (e: MalformedURLException) {
return null
}
if (!Utilities.contain(VALID_HOSTS, url.host)) {
if (url.host !in VALID_HOSTS) {
return null
}
val path = url.path ?: return null
@@ -88,7 +86,7 @@ object GalleryListUrlParser {
return null
}
}
if (TextUtils.isEmpty(uploader)) {
if (uploader.isNullOrEmpty()) {
return null
}
val builder = ListUrlBuilder()
@@ -117,7 +115,7 @@ object GalleryListUrlParser {
return null
}
}
if (TextUtils.isEmpty(tag)) {
if (tag.isNullOrEmpty()) {
return null
}
val builder = ListUrlBuilder()
@@ -129,7 +127,7 @@ object GalleryListUrlParser {
// TODO get page
private fun parseToplist(path: String): ListUrlBuilder? {
val uri = Uri.parse(path)
if (uri == null || TextUtils.isEmpty(uri.getQueryParameter("tl"))) {
if (uri == null || uri.getQueryParameter("tl").isNullOrEmpty()) {
return null
}
val tl = uri.getQueryParameter("tl")!!.toInt()
@@ -17,22 +17,26 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.ExceptionUtils
import org.json.JSONArray
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryMultiPageViewerPTokenParser {
private const val IMAGE_LIST_STRING = "var imagelist = "
fun parse(body: String): List<String> {
val imagelist = body.substring(
body.indexOf("var imagelist = ") + 16,
body.indexOf(";", body.indexOf("var imagelist = ")),
)
val index = body.indexOf(IMAGE_LIST_STRING)
val imagelist = body.substring(index + IMAGE_LIST_STRING.length, body.indexOf(";", index))
return runCatching {
val ja = JSONArray(imagelist)
(0 until ja.length()).map { ja.getJSONObject(it).getString("k") }
imagelist.parseAs<List<Item>>().map(Item::token)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException(body)
throw ParseException("Parse pToken from MPV error", body, it)
}
}
@Serializable
data class Item(@SerialName("k") val token: String)
}
@@ -18,15 +18,13 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import org.jsoup.Jsoup
object GalleryNotAvailableParser {
fun parse(body: String): String? {
return runCatching {
val document = Jsoup.parse(body)
val d = JsoupUtils.getElementByClass(document, "d")
d!!.child(0).html().replace("<br>", "\n")
val e = Jsoup.parse(body).getElementsByClass("d").first()
e!!.child(0).html().replace("<br>", "\n")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
@@ -16,56 +16,44 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.yorozuya.StringUtils
import org.json.JSONException
import org.json.JSONObject
import java.util.regex.Matcher
import java.util.regex.Pattern
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryPageApiParser {
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL =
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String): Result {
return try {
var m: Matcher
val jo = JSONObject(body)
if (jo.has("error")) {
throw ParseException(jo.getString("error"), body)
}
val i3 = jo.getString("i3")
m = PATTERN_IMAGE_URL.matcher(i3)
val imageUrl = if (m.find()) {
StringUtils.unescapeXml(StringUtils.trim(m.group(1)))
} else {
null
}
val i6 = jo.getString("i6")
m = PATTERN_SKIP_HATH_KEY.matcher(i6)
val skipHathKey = if (m.find()) {
StringUtils.unescapeXml(StringUtils.trim(m.group(1)))
} else {
null
}
val i7 = jo.getString("i7")
m = PATTERN_ORIGIN_IMAGE_URL.matcher(i7)
val originImageUrl = if (m.find()) {
StringUtils.unescapeXml(m.group(1)) + "fullimg.php" +
StringUtils.unescapeXml(m.group(2))
} else {
null
}
if (!imageUrl.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl)
} else {
throw ParseException("Parse image url and skip hath key error", body)
}
} catch (e: JSONException) {
throw ParseException("Can't parse json", body, e)
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
groupValues[1].trim().unescapeXml()
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
}
if (!imageUrl.isNullOrEmpty()) {
return Result(imageUrl, skipHathKey, originImageUrl)
} else {
throw ParseException("Parse image url and skip hath key error", body)
}
}
@Serializable
private data class JsonResult(
@SerialName("i3")
val imageUrl: String,
@SerialName("i6")
val skipHathKey: String,
@SerialName("i7")
val originImageUrl: String,
)
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
}
@@ -16,7 +16,7 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.yorozuya.StringUtils
import com.hippo.ehviewer.util.unescapeXml
import java.util.regex.Pattern
object GalleryPageParser {
@@ -31,20 +31,19 @@ object GalleryPageParser {
fun parse(body: String): Result {
var m = PATTERN_IMAGE_URL.matcher(body)
val imageUrl = if (m.find()) {
StringUtils.unescapeXml(StringUtils.trim(m.group(1)))
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_SKIP_HATH_KEY.matcher(body)
val skipHathKey = if (m.find()) {
StringUtils.unescapeXml(StringUtils.trim(m.group(1)))
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
val originImageUrl = if (m.find()) {
StringUtils.unescapeXml(m.group(1)) + "fullimg.php" +
StringUtils.unescapeXml(m.group(2))
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
} else {
null
}
@@ -16,7 +16,8 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.toLongOrDefault
import java.util.regex.Pattern
/**
@@ -24,7 +25,7 @@ import java.util.regex.Pattern
*/
object GalleryPageUrlParser {
private val URL_STRICT_PATTERN = Pattern.compile(
"https?://(?:" + EhUrl.DOMAIN_EX + "|" + EhUrl.DOMAIN_E + "|" + EhUrl.DOMAIN_LOFI + ")/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
)
private val URL_PATTERN = Pattern.compile("([0-9a-f]{10})/(\\d+)-(\\d+)")
@@ -33,9 +34,9 @@ object GalleryPageUrlParser {
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
val m = pattern.matcher(url)
return if (m.find()) {
val gid = NumberUtils.parseLongSafely(m.group(2), -1L)
val gid = m.group(2)!!.toLongOrDefault(-1L)
val pToken = m.group(1)!!
val page = NumberUtils.parseIntSafely(m.group(3), 0) - 1
val page = m.group(3)!!.toIntOrDefault(0) - 1
if (gid < 0 || page < 0) {
null
} else {
@@ -16,8 +16,10 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.ExceptionUtils
import org.json.JSONObject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryTokenApiParser {
/**
@@ -31,12 +33,17 @@ object GalleryTokenApiParser {
* }
*/
fun parse(body: String): String {
val jo = JSONObject(body).getJSONArray("tokenlist").getJSONObject(0)
return runCatching {
jo.getString("token")
body.parseAs<Result>().tokenList[0].token
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException(jo.getString("error"))
throw EhException(body.parseAs<Error>().error)
}
}
@Serializable
data class Result(@SerialName("tokenlist") val tokenList: List<Item>)
@Serializable
data class Item(val gid: Long, val token: String)
}
@@ -1,31 +1,20 @@
package com.hippo.ehviewer.client.parser
import android.os.Parcelable
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.ParseException
import org.jsoup.Jsoup
import kotlinx.parcelize.Parcelize
import java.nio.ByteBuffer
object HomeParser {
private val PATTERN_FUNDS =
Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
private const val RESET_SUCCEED = "Image limit was successfully reset."
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
fun parse(body: String): Limits {
Jsoup.parse(body).selectFirst("div.homebox")?.let {
val es = it.select("p > strong")
if (es.size == 3) {
val current = ParserUtils.parseInt(es[0].text(), 0)
val maximum = ParserUtils.parseInt(es[1].text(), 0)
val resetCost = ParserUtils.parseInt(es[2].text(), 0)
return Limits(current, maximum, resetCost)
}
}
throw ParseException("Parse image limits error", body)
}
fun parse(body: ByteBuffer) = parseLimit(body)
fun parseResetLimits(body: String): Limits? {
return if (body.contains(RESET_SUCCEED)) {
null
} else {
parse(body)
fun parseResetLimits(body: String) {
if (body.contains(INSUFFICIENT_FUNDS)) {
throw InsufficientFundsException()
}
}
@@ -38,7 +27,12 @@ object HomeParser {
throw ParseException("Parse funds error", body)
}
data class Limits(val current: Int = 0, val maximum: Int, val resetCost: Int = 0)
data class Funds(val fundsGP: Int, val fundsC: Int)
@Parcelize
data class Funds(val fundsGP: Int, val fundsC: Int) : Parcelable
class Result(val limits: Limits, val funds: Funds)
}
@Parcelize
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
@@ -15,8 +15,9 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.yorozuya.StringUtils
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.toLongOrDefault
import com.hippo.ehviewer.util.unescapeXml
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
@@ -32,14 +33,14 @@ object ParserUtils {
}
fun trim(str: String?): String {
return str?.let { StringUtils.unescapeXml(it).trim() } ?: ""
return str?.unescapeXml()?.trim() ?: ""
}
fun parseInt(str: String?, defValue: Int): Int {
return NumberUtils.parseIntSafely(trim(str).replace(",", ""), defValue)
return trim(str).replace(",", "").toIntOrDefault(defValue)
}
fun parseLong(str: String?, defValue: Long): Long {
return NumberUtils.parseLongSafely(trim(str).replace(",", ""), defValue)
return trim(str).replace(",", "").toLongOrDefault(defValue)
}
}
@@ -1,35 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import org.json.JSONException
import org.json.JSONObject
object RateGalleryParser {
fun parse(body: String): Result {
return try {
val jsonObject = JSONObject(body)
val rating = jsonObject.getDouble("rating_avg").toFloat()
val ratingCount = jsonObject.getInt("rating_cnt")
Result(rating, ratingCount)
} catch (e: JSONException) {
throw ParseException("Can't parse rate gallery", body, e)
}
}
class Result(val rating: Float, val ratingCount: Int)
}
@@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.legacy
package com.hippo.ehviewer.client.parser
import android.graphics.drawable.Drawable
import android.text.Html.ImageGetter
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class URLImageGetter(
private val mTextView: ObservedTextView,
) : ImageGetter {
override fun getDrawable(source: String): Drawable {
return UnikeryDrawable(mTextView, source)
}
}
@Serializable
data class RateGalleryResult(
@SerialName("rating_avg")
val rating: Float,
@SerialName("rating_cnt")
val ratingCount: Int,
)
@@ -1,56 +1,23 @@
/*
* 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.client.parser
import org.jsoup.Jsoup
import java.util.regex.Pattern
import java.nio.ByteBuffer
object TorrentParser {
private val PATTERN_TORRENT =
Pattern.compile("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]B)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>")
fun parse(body: String): List<Result> {
val torrentList = ArrayList<Result>()
val d = Jsoup.parse(body)
val es = d.select("form>div>table")
for (e in es) {
val m = PATTERN_TORRENT.matcher(e.html())
if (m.find()) {
val posted = m.group(1)!!
val size = m.group(2)!!
val seeds = m.group(3)!!.toInt()
val peers = m.group(4)!!.toInt()
val downloads = m.group(5)!!.toInt()
val url = ParserUtils.trim(m.group(7))
val name = ParserUtils.trim(m.group(8))
torrentList.add(Result(posted, size, seeds, peers, downloads, url, name))
}
}
return torrentList
}
class Result(
private val posted: String,
private val size: String,
private val seeds: Int,
private val peers: Int,
private val downloads: Int,
val url: String,
val name: String,
) {
fun format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
}
fun parse(body: ByteBuffer) = parseTorrent(body)
}
class Torrent(
val posted: String,
val size: String,
val seeds: Int,
val peers: Int,
val downloads: Int,
val url: String,
val name: String,
)
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
typealias TorrentResult = ArrayList<Torrent>
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
@@ -1,31 +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.client.parser
import org.json.JSONObject
object VoteCommentParser {
// {"comment_id":1253922,"comment_score":-19,"comment_vote":0}
fun parse(body: String, expectVote: Int): Result {
val jo = JSONObject(body)
val id = jo.getLong("comment_id")
val score = jo.getInt("comment_score")
val vote = jo.getInt("comment_vote")
return Result(id, score, vote, expectVote)
}
class Result(val id: Long, val score: Int, val vote: Int, val expectVote: Int)
}
@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
package com.hippo.ehviewer.client.parser
import androidx.annotation.StringRes
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GetText {
fun getString(@StringRes id: Int): String {
return EhApplication.application.getString(id)
}
fun getString(@StringRes id: Int, vararg formatArgs: Any?): String {
return EhApplication.application.getString(id, *formatArgs)
}
}
@Serializable
data class VoteCommentResult(
@SerialName("comment_id")
val id: Long,
@SerialName("comment_score")
val score: Int,
@SerialName("comment_vote")
val vote: Int,
)
@@ -17,11 +17,11 @@
*/
package com.hippo.ehviewer.client.parser
import org.json.JSONObject
import com.hippo.ehviewer.client.parseAs
object VoteTagParser {
// {"error":"The tag \"neko\" is not allowed. Use character:neko or artist:neko"}
fun parse(body: String): String {
return JSONObject(body).optString("error")
fun parse(body: String): String? {
return runCatching { body.parseAs<Error>().error }.getOrNull()
}
}
@@ -0,0 +1,65 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.hippo.ehviewer.coil
import android.net.Uri
import android.webkit.MimeTypeMap
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.Options
import com.hippo.ehviewer.cronet.copyToChannel
import com.hippo.ehviewer.cronet.cronetRequest
import com.hippo.ehviewer.cronet.execute
import java.io.RandomAccessFile
/**
* A HttpUriFetcher impl use Cronet for jvmheapless IO
* cacheHeader is never respected | recorded since thumb is immutable
*/
class CronetHttpUriFetcher(private val data: String, private val options: Options, private val imageLoader: ImageLoader) : Fetcher {
override suspend fun fetch(): FetchResult {
val diskCacheKey = options.diskCacheKey ?: data
val diskCache = requireNotNull(imageLoader.diskCache)
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
val success = cronetRequest(data) {
disableCache()
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
copyToChannel(it.channel)
}
}
}
check(success)
// diskcache snapshot MUST exist here
requireNotNull(diskCache.openSnapshot(diskCacheKey))
}
return SourceResult(
source = ImageSource(snapshot.data, diskCache.fileSystem, diskCacheKey, snapshot),
mimeType = getMimeType(data),
dataSource = DataSource.DISK,
)
}
}
fun ComponentRegistry.Builder.installCronetHttpUriFetcher() = add { data: Uri, options, loader ->
if (data.scheme != "http" && data.scheme != "https") return@add null
CronetHttpUriFetcher(data.toString(), options, loader)
}
private fun getMimeType(url: String) = MimeTypeMap.getSingleton().getMimeTypeFromUrl(url)
private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? {
if (url.isNullOrBlank()) return null
val extension = url
.substringBeforeLast('#') // Strip the fragment.
.substringBeforeLast('?') // Strip the query.
.substringAfterLast('/') // Get the last path segment.
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
return getMimeTypeFromExtension(extension)
}
@@ -2,7 +2,20 @@ package com.hippo.ehviewer.coil
import coil.disk.DiskCache
suspend inline fun DiskCache.edit(key: String, crossinline block: suspend DiskCache.Editor.() -> Unit): Boolean {
inline fun DiskCache.edit(key: String, crossinline block: DiskCache.Editor.() -> Unit): Boolean {
val editor = openEditor(key) ?: return false
editor.runCatching {
block(this)
}.onFailure {
editor.abort()
throw it
}.onSuccess {
editor.commit()
}
return true
}
suspend inline fun DiskCache.suspendEdit(key: String, crossinline block: suspend DiskCache.Editor.() -> Unit): Boolean {
val editor = openEditor(key) ?: return false
editor.runCatching {
block(this)
@@ -1,11 +1,9 @@
package com.hippo.ehviewer.coil
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.disk.DiskCache
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Size
@@ -36,8 +34,3 @@ fun ImageRequest.Builder.justDownload() = apply {
memoryCachePolicy(CachePolicy.DISABLED)
decoderFactory { _, _, _ -> stubFactory }
}
inline fun Context.imageRequest(preview: GalleryPreview, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehPreview(preview).apply(builder).build()
inline fun Context.imageRequest(info: GalleryInfo, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehUrl(info).apply(builder).build()
inline fun Context.imageRequest(builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).apply(builder).build()
inline fun diskCache(builder: DiskCache.Builder.() -> Unit) = DiskCache.Builder().apply(builder).build()
@@ -1,79 +1,81 @@
package com.hippo.ehviewer.coil
import coil.intercept.Interceptor
import coil.request.ImageRequest
import coil.request.ImageResult
import com.hippo.ehviewer.Settings
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> = hashMapOf()
private val pendingContinuationMapLock = Mutex()
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
private val pendingContinuationMapLock = Any()
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val EMPTY_LIST = mutableListOf<Continuation<Unit>>()
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val originRequest = chain.request
val originListener = originRequest.listener
val key = originRequest.data as String
val newRequest = ImageRequest.Builder(originRequest).listener(
{ originListener?.onStart(it) },
{ originListener?.onCancel(it) },
{ request, result ->
originListener?.onError(request, result)
// Wake all pending continuations since this request is to be failed
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
{ request, result ->
originListener?.onSuccess(request, result)
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
).build()
private val EMPTY_LIST = mutableListOf<CancellableContinuation<Unit>>()
pendingContinuationMapLock.lock()
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = EMPTY_LIST
pendingContinuationMapLock.unlock()
} else {
if (existPendingContinuations === EMPTY_LIST) pendingContinuationMap[key] = mutableListOf()
pendingContinuationMap[key]!!.apply {
suspendCancellableCoroutine { continuation ->
add(continuation)
pendingContinuationMapLock.unlock()
continuation.invokeOnCancellation { remove(continuation) }
private fun triggerSuccessor(key: String) {
notifyScope.launch {
synchronized(pendingContinuationMapLock) {
// Wake up a pending continuation to continue executing task
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) { triggerSuccessor(key) } }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
}
}
}
private fun triggerFailure(key: String, e: Throwable) {
notifyScope.launch {
synchronized(pendingContinuationMapLock) {
pendingContinuationMap.remove(key)?.forEach { it.resumeWithException(e) }
}
}
}
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val req = chain.request
val key = req.memoryCacheKey?.key?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorDispatcher) { chain.proceed(req) }
suspendCancellableCoroutine { continuation ->
synchronized(pendingContinuationMapLock) {
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = EMPTY_LIST
continuation.resume(Unit) {
triggerSuccessor(key)
}
} else {
if (existPendingContinuations === EMPTY_LIST) pendingContinuationMap[key] = mutableListOf()
pendingContinuationMap[key]!!.apply {
add(continuation)
continuation.invokeOnCancellation { remove(continuation) }
}
}
}
}
try {
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
} catch (e: CancellationException) {
notifyScope.launch {
pendingContinuationMapLock.withLock {
// Wake up a pending continuation to continue executing task
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
return withContext(req.interceptorDispatcher) { chain.proceed(req) }.apply {
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
synchronized(pendingContinuationMapLock) {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
}
} catch (e: CancellationException) {
triggerSuccessor(key)
throw e
} catch (e: Throwable) {
// Wake all pending continuations since this request is to be failed
triggerFailure(key, e)
throw e
}
}
@@ -0,0 +1,117 @@
package com.hippo.ehviewer.cronet
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhCookieStore
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.utils.io.pool.DirectByteBufferPool
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.Path.Companion.toOkioPath
import org.chromium.net.CronetEngine
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import splitties.init.appCtx
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(CHROME_USER_AGENT)
}.build()
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
// TODO: Rewrite this to use android.net.http.HttpEngine and make it Android 14 only when released
class CronetRequest {
lateinit var consumer: (ByteBuffer) -> Unit
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
lateinit var request: UrlRequest
lateinit var onError: (Throwable) -> Unit
lateinit var readerCont: Continuation<Unit>
val callback = object : UrlRequest.Callback() {
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
}
override fun onResponseStarted(req: UrlRequest, info: UrlResponseInfo) {
onResponse(info)
}
override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) {
data.flip()
consumer(data)
}
override fun onSucceeded(req: UrlRequest, info: UrlResponseInfo) {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
onError(e)
}
}
}
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
}.apply(conf).build()
}
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
val buffer = pool.borrow()
return try {
suspendCancellableCoroutine { cont ->
consumer = {
callback(it)
buffer.clear()
request.read(buffer)
}
onError = { readerCont.resumeWithException(it) }
readerCont = cont
request.read(buffer)
}
} finally {
pool.recycle(buffer)
}
}
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
val bytes = chan.write(it)
listener(bytes)
}
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
contract {
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
}
return coroutineScope {
suspendCancellableCoroutine { cont ->
onResponse = { launch { cont.resume(callback(it)) } }
cont.invokeOnCancellation { request.cancel() }
onError = { cont.resumeWithException(it) }
request.start()
}
}
}
@@ -2,10 +2,15 @@ package com.hippo.ehviewer.dailycheck
import android.annotation.SuppressLint
import android.content.Context
import android.text.Html
import android.content.Intent
import android.net.Uri
import android.text.style.URLSpan
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
@@ -14,12 +19,12 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.lang.withIOContext
import splitties.init.appCtx
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
@@ -41,15 +46,15 @@ class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
}
val schedHour
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, -1).takeUnless { it == -1 }
get() = Settings.requestNewsTimerHour.takeUnless { it == -1 } ?: 0
val schedMinute
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, -1).takeUnless { it == -1 }
get() = Settings.requestNewsTimerMinute.takeUnless { it == -1 } ?: 0
private val whenToWork
get() = LocalDateTime.now()
.run { withHour(schedHour ?: hour) }
.run { withMinute(schedMinute ?: minute) }
.run { withHour(schedHour) }
.run { withMinute(schedMinute) }
private val initialDelay
get() = Duration.between(LocalDateTime.now(), whenToWork)
@@ -98,20 +103,26 @@ fun showEventNotification(html: String) {
if (Settings.hideHvEvents && html.contains("You have encountered a monster!")) {
return
}
val context = EhApplication.application
val notificationManager = NotificationManagerCompat.from(context)
val notificationManager = NotificationManagerCompat.from(appCtx)
val chan = NotificationChannelCompat
.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(CHANNEL_ID)
.build()
notificationManager.createNotificationChannel(chan)
val msg = NotificationCompat.Builder(context, CHANNEL_ID)
val text = html.parseAsHtml()
val msg = NotificationCompat.Builder(appCtx, CHANNEL_ID)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher_monochrome)
.setContentText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle())
.build()
val urls = text.getSpans<URLSpan>(0, text.length)
if (urls.isNotEmpty()) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(urls.first().url))
val pi = PendingIntentCompat.getActivity(appCtx, 0, intent, 0, false)
msg.setContentIntent(pi)
}
runCatching {
notificationManager.notify(1, msg)
notificationManager.notify(1, msg.build())
}.onFailure {
it.printStackTrace()
}
@@ -1,6 +0,0 @@
package com.hippo.ehviewer.dao
interface BasicDao<T> {
fun list(): List<T>
fun insert(t: T): Long
}
@@ -1,16 +0,0 @@
package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import com.hippo.ehviewer.client.data.BaseGalleryInfo
@Entity(tableName = "BOOKMARKS")
class BookmarkInfo : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "PAGE")
var page = 0
@JvmField
@ColumnInfo(name = "TIME")
var time: Long = 0
}
@@ -1,18 +0,0 @@
package com.hippo.ehviewer.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface BookmarksDao : BasicDao<BookmarkInfo> {
@Insert
override fun insert(t: BookmarkInfo): Long
@Delete
fun delete(bookmark: BookmarkInfo)
@Query("SELECT * FROM BOOKMARKS ORDER BY TIME DESC")
override fun list(): List<BookmarkInfo>
}
@@ -1,22 +1,10 @@
package com.hippo.ehviewer.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
@Dao
interface CookiesDao {
@Query("SELECT * FROM OK_HTTP_3_COOKIE")
fun list(): List<Cookie>
@Delete
fun delete(cookie: Cookie)
@Insert
fun insert(cookie: Cookie): Long
@Update
fun update(cookie: Cookie)
suspend fun list(): List<Cookie>
}
@@ -0,0 +1,211 @@
package com.hippo.ehviewer.dao
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.DeleteTable
import androidx.room.RenameColumn
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
@DeleteTable(tableName = "BOOKMARKS")
class DropBookMarkDao : AutoMigrationSpec
@RenameColumn(tableName = "HISTORY", fromColumnName = "MODE", toColumnName = "FAVORITE_SLOT")
class HistoryMigration : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE HISTORY SET FAVORITE_SLOT = FAVORITE_SLOT - 2")
}
}
class AddPositionColumn : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
val needMigrationTables = arrayOf(
"QUICK_SEARCH",
"DOWNLOAD_LABELS",
"DOWNLOADS",
)
needMigrationTables.forEach { table ->
// TODO: Rewrite this with row_number() when min sdk is 30 (SQLite 3.28.0)
db.execSQL("UPDATE $table SET POSITION = (SELECT COUNT(*) FROM $table T WHERE T.TIME < $table.TIME)")
}
}
}
@DeleteColumn(tableName = "QUICK_SEARCH", columnName = "TIME")
@DeleteColumn(tableName = "DOWNLOAD_LABELS", columnName = "TIME")
class DropTimeColumn : AutoMigrationSpec
class AddGalleryTable : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
val needMigrationTables = arrayOf(
"HISTORY",
"DOWNLOADS",
"LOCAL_FAVORITES",
)
needMigrationTables.forEachIndexed { index, table ->
db.execSQL(
"INSERT OR IGNORE INTO GALLERIES " +
"SELECT GID, TOKEN, TITLE, TITLE_JPN, THUMB, CATEGORY, POSTED, UPLOADER, RATING, SIMPLE_LANGUAGE, " +
"${if (index == 0) "FAVORITE_SLOT" else NOT_FAVORITED} FROM $table",
)
}
}
}
@DeleteColumn(tableName = "HISTORY", columnName = "TOKEN")
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE")
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE_JPN")
@DeleteColumn(tableName = "HISTORY", columnName = "THUMB")
@DeleteColumn(tableName = "HISTORY", columnName = "CATEGORY")
@DeleteColumn(tableName = "HISTORY", columnName = "POSTED")
@DeleteColumn(tableName = "HISTORY", columnName = "UPLOADER")
@DeleteColumn(tableName = "HISTORY", columnName = "RATING")
@DeleteColumn(tableName = "HISTORY", columnName = "SIMPLE_LANGUAGE")
@DeleteColumn(tableName = "HISTORY", columnName = "FAVORITE_SLOT")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TOKEN")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE_JPN")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "THUMB")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "CATEGORY")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "POSTED")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "UPLOADER")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "RATING")
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "SIMPLE_LANGUAGE")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TOKEN")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE_JPN")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "THUMB")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "CATEGORY")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "POSTED")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "UPLOADER")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "RATING")
@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE")
class GalleryTableMigration : AutoMigrationSpec
class ThumbKeyMigration : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
val needMigrationTables = arrayOf(
"DOWNLOADS",
"HISTORY",
"LOCAL_FAVORITES",
)
val prefixToRemove = arrayOf(
"https://ehgt.org/",
"https://s.exhentai.org/t/",
"https://exhentai.org/t/",
)
needMigrationTables.forEach { table ->
prefixToRemove.forEach { prefix ->
db.execSQL("UPDATE $table SET thumb = SUBSTR(thumb ,LENGTH('$prefix') + 1) WHERE thumb LIKE '$prefix%'")
}
}
}
}
@Database(
entities = [BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, QuickSearch::class],
version = 16,
exportSchema = true,
autoMigrations = [
AutoMigration(
from = 4,
to = 5,
spec = ThumbKeyMigration::class,
),
AutoMigration(
from = 5,
to = 6,
),
AutoMigration(
from = 6,
to = 7,
spec = DropBookMarkDao::class,
),
AutoMigration(
from = 7,
to = 8,
),
AutoMigration(
from = 8,
to = 9,
),
AutoMigration(
from = 9,
to = 10,
spec = HistoryMigration::class,
),
AutoMigration(
from = 10,
to = 11,
spec = AddPositionColumn::class,
),
AutoMigration(
from = 11,
to = 12,
spec = DropTimeColumn::class,
),
AutoMigration(
from = 12,
to = 13,
spec = AddGalleryTable::class,
),
AutoMigration(
from = 13,
to = 14,
spec = GalleryTableMigration::class,
),
AutoMigration(
from = 14,
to = 15,
),
AutoMigration(
from = 15,
to = 16,
),
],
)
@TypeConverters(FilterModeConverter::class)
abstract class EhDatabase : RoomDatabase() {
abstract fun galleryDao(): GalleryDao
abstract fun downloadDirnameDao(): DownloadDirnameDao
abstract fun downloadLabelDao(): DownloadLabelDao
abstract fun downloadsDao(): DownloadsDao
abstract fun filterDao(): FilterDao
abstract fun historyDao(): HistoryDao
abstract fun localFavoritesDao(): LocalFavoritesDao
abstract fun quickSearchDao(): QuickSearchDao
}
// 1 -> 2 some nullability changes
@Database(
entities = [Cookie::class],
version = 2,
autoMigrations = [
AutoMigration(
from = 1,
to = 2,
),
],
)
abstract class CookiesDatabase : RoomDatabase() {
abstract fun cookiesDao(): CookiesDao
}
@Database(
entities = [Search::class],
version = 2,
autoMigrations = [
AutoMigration(
from = 1,
to = 2,
),
],
)
abstract class SearchDatabase : RoomDatabase() {
abstract fun searchDao(): SearchDao
}
@@ -8,8 +8,8 @@ import androidx.room.PrimaryKey
data class DownloadDirname(
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long = 0,
val gid: Long,
@ColumnInfo(name = "DIRNAME")
var dirname: String? = null,
val dirname: String,
)
@@ -2,26 +2,24 @@ package com.hippo.ehviewer.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import androidx.room.Upsert
@Dao
interface DownloadDirnameDao : BasicDao<DownloadDirname> {
interface DownloadDirnameDao {
@Query("SELECT * FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
fun load(gid: Long): DownloadDirname?
suspend fun load(gid: Long): DownloadDirname?
@Update
fun update(downloadDirname: DownloadDirname)
@Upsert
suspend fun upsert(downloadDirname: DownloadDirname)
@Insert
override fun insert(t: DownloadDirname): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(downloadDirnameList: List<DownloadDirname>)
@Query("DELETE FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
fun deleteByKey(gid: Long)
@Query("DELETE FROM DOWNLOAD_DIRNAME")
fun deleteAll()
suspend fun deleteByKey(gid: Long)
@Query("SELECT * FROM DOWNLOAD_DIRNAME")
override fun list(): List<DownloadDirname>
suspend fun list(): List<DownloadDirname>
}
@@ -1,63 +1,76 @@
package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.Relation
import com.hippo.ehviewer.client.data.AbstractDownloadInfo
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import java.time.Instant
@Entity(
tableName = "DOWNLOADS",
foreignKeys = [
ForeignKey(BaseGalleryInfo::class, ["GID"], ["GID"]),
ForeignKey(
DownloadLabel::class,
["LABEL"],
["LABEL"],
onDelete = ForeignKey.SET_NULL,
onUpdate = ForeignKey.CASCADE,
),
],
)
data class DownloadEntity(
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long,
@Entity(tableName = "DOWNLOADS")
class DownloadInfo() : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "STATE")
var state = 0
override var state: Int = 0,
@JvmField
@ColumnInfo(name = "LEGACY")
var legacy = 0
override var legacy: Int = 0,
@JvmField
@ColumnInfo(name = "TIME")
var time: Long = 0
override var time: Long = Instant.now().toEpochMilli(),
@JvmField
@ColumnInfo(name = "LABEL")
var label: String? = null
@ColumnInfo(name = "LABEL", index = true)
override var label: String? = null,
@JvmField
@ColumnInfo(name = "POSITION")
override var position: Int = 0,
) : AbstractDownloadInfo {
@Ignore
var speed: Long = 0
override var speed: Long = 0
@JvmField
@Ignore
var remaining: Long = 0
override var remaining: Long = 0
@JvmField
@Ignore
var finished = 0
override var finished = 0
@JvmField
@Ignore
var downloaded = 0
override var downloaded = 0
@JvmField
@Ignore
var total = 0
override var total = 0
}
constructor(galleryInfo: GalleryInfo) : this() {
gid = galleryInfo.gid
token = galleryInfo.token
title = galleryInfo.title
titleJpn = galleryInfo.titleJpn
thumbKey = galleryInfo.thumbKey
this.category = galleryInfo.category
posted = galleryInfo.posted
uploader = galleryInfo.uploader
rating = galleryInfo.rating
simpleTags = galleryInfo.simpleTags
simpleLanguage = galleryInfo.simpleLanguage
}
data class DownloadInfo(
@Relation(parentColumn = "GID", entityColumn = "GID")
val galleryInfo: BaseGalleryInfo,
@ColumnInfo(name = "DIRNAME")
val dirname: String?,
@Embedded
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
companion object {
const val STATE_INVALID = -1
const val STATE_NONE = 0
@@ -2,17 +2,18 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "DOWNLOAD_LABELS")
@Entity(tableName = "DOWNLOAD_LABELS", indices = [Index("LABEL", unique = true)])
data class DownloadLabel(
@ColumnInfo(name = "LABEL")
var label: String,
@ColumnInfo(name = "POSITION")
var position: Int,
@PrimaryKey
@ColumnInfo(name = "_id")
var id: Long? = null,
@ColumnInfo(name = "LABEL")
var label: String? = null,
@ColumnInfo(name = "TIME")
var time: Long = 0,
)
@@ -7,22 +7,28 @@ import androidx.room.Query
import androidx.room.Update
@Dao
interface DownloadLabelDao : BasicDao<DownloadLabel> {
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC")
override fun list(): List<DownloadLabel>
interface DownloadLabelDao {
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC")
suspend fun list(): List<DownloadLabel>
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC LIMIT :limit OFFSET :offset")
fun list(offset: Int, limit: Int): List<DownloadLabel>
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC LIMIT :limit OFFSET :offset")
suspend fun list(offset: Int, limit: Int): List<DownloadLabel>
@Query("UPDATE DOWNLOAD_LABELS SET POSITION = POSITION - 1 WHERE POSITION > :position")
suspend fun fill(position: Int)
@Update
fun update(downloadLabels: List<DownloadLabel>)
suspend fun update(downloadLabels: List<DownloadLabel>)
@Update
fun update(downloadLabel: DownloadLabel)
suspend fun update(downloadLabel: DownloadLabel)
@Insert
override fun insert(t: DownloadLabel): Long
suspend fun insert(downloadLabel: DownloadLabel): Long
@Insert
suspend fun insert(downloadLabels: List<DownloadLabel>)
@Delete
fun delete(downloadLabel: DownloadLabel)
suspend fun delete(downloadLabel: DownloadLabel)
}
Loaded 100 of 541 files, more files were not shown because too many files have changed in this diff. Show more