Compare commits

...

188 Commits

Author SHA1 Message Date
Pupil
547b6e8e3b Fixed renamed file 2020-02-09 18:16:45 +09:00
Pupil
d88ac27e72 Fixed renamed file 2020-02-09 18:15:46 +09:00
Pupil
e551a40d08 Fixed renamed file 2020-02-09 18:13:16 +09:00
Pupil
e810abe33a Bug fix 2020-02-09 17:57:18 +09:00
Pupil
6172a73719 Bug fix 2020-02-09 17:36:28 +09:00
Pupil
7455e68a45 Bug fix 2020-02-09 17:35:34 +09:00
Pupil
748495ca64 Downloader thread number to 4 2020-02-09 17:25:55 +09:00
Pupil
f6d9c7f550 Bug fix
Networking optimized
2020-02-09 17:11:35 +09:00
Pupil
384e6c61b0 Fixed 'Can't locate argument-less serializer for class xyz.quaver.pupil.d.e. For generic classes, such as lists, please provide serializer explicitly.' 2020-02-08 22:16:26 +09:00
Pupil
d49c9cec20 Merge pull request #64 from tom5079/development
Version 5.0
2020-02-08 20:38:51 +09:00
Pupil
4b27f1aba1 Added support for gallery unregistered in hitomi 2020-02-08 20:37:59 +09:00
Pupil
a0a989c785 Added support for gallery unregistered in hitomi 2020-02-08 20:36:08 +09:00
Pupil
ecaecc1b91 Added Custom download folder 2020-02-08 19:01:45 +09:00
Pupil
938156aa71 Merge pull request #58 from tom5079/issue-35
Might be a fix for #35
2020-02-03 12:01:06 +09:00
Pupil
d30c51bb3a Finished integrating new downloader 2020-02-03 11:54:29 +09:00
Pupil
874606bff9 Added Download feature 2020-02-01 12:50:26 +09:00
Pupil
07643e4b4c fixed conflict 2020-01-31 11:11:34 +09:00
Pupil
20bc5423cf Merge pull request #63 from tom5079/development
Version 4.3-hotfix1
2020-01-31 10:39:06 +09:00
Pupil
b84cddffdc Version up & dependency update 2020-01-31 10:32:21 +09:00
Pupil
e46d1123df resolves #62 2020-01-31 10:24:19 +09:00
Pupil
48f90faf4e Applying changed Download routines 2020-01-31 10:12:44 +09:00
Pupil
615b52c4fa Merge branch 'development' into issue-35 2020-01-30 11:58:24 +09:00
Pupil
2c9c8e223c Fixes bug when trying to open hiyobi-only galleries 2020-01-30 00:01:53 +09:00
Pupil
01a653835e Deprecate GalleryDownloader 2020-01-29 20:58:20 +09:00
Pupil
9d80857a38 Rebuilding Downloader 2020-01-29 15:46:23 +09:00
Pupil
8a9ab6b36c Rebuilding Downloader 2020-01-28 12:56:32 +09:00
Pupil
4edc87c197 Rebuilding Downloader 2020-01-28 12:54:34 +09:00
Pupil
10712e6e62 Rebuilding Downloader 2020-01-28 10:48:31 +09:00
Pupil
d73dc19d3d Fixed enlarged chip spacing 2020-01-24 17:02:53 +09:00
Pupil
c204353220 Added mirror selector
Developing new Downloader under xyz.quaver.pupil.util.download
2020-01-24 15:11:35 +09:00
Pupil
37123a2cd5 Merge remote-tracking branch 'origin/issue-35' into issue-35
# Conflicts:
#	app/src/main/res/layout/item_reader.xml
2020-01-22 23:22:44 +09:00
Pupil
a39484b6ea Might be a fix for #35
but it's quite dirty :v
2020-01-22 23:17:28 +09:00
Pupil
e81b5a4e3a Added pinch-zoom 2020-01-22 23:14:57 +09:00
Pupil
0b87c57fbf Dependency update 2020-01-22 23:11:54 +09:00
Pupil
5fd985ba39 Merge pull request #61 from tom5079/Pupil-57
Pupil-57 about the horizontal and search
2020-01-22 11:45:14 +09:00
Pupil
8c64548513 Pupil-57 about the horizontal and search 2020-01-22 11:42:27 +09:00
Pupil
a6de64ceb9 Some code cleanups :P #37 2020-01-22 11:21:18 +09:00
Pupil
16ebb437a3 Merge pull request #60 from tom5079/Pupil-25
Pupil-25 Add option to download jpg instead of webp files
2020-01-22 11:08:49 +09:00
Pupil
683118a3f4 Fixed old android not supporting ContentProvider 2020-01-22 11:07:55 +09:00
Pupil
08e38ed45c Pupil-25 Add option to download jpg instead of webp files 2020-01-22 10:50:55 +09:00
Pupil
7abf08f1fb Some code cleanups :P #37 2020-01-20 18:59:18 +09:00
Pupil
f3019e9b84 Some code cleanups :P #37 2020-01-20 18:58:19 +09:00
Pupil
9ea55664b6 Might be a fix for #35
but it's quite dirty :v
2020-01-17 10:30:39 +09:00
Pupil
c468764234 Fixed enlarged chip spacing 2020-01-16 20:37:43 +09:00
Pupil
31c3178430 Merge pull request #56 from tom5079/Pupil-54
Fixed thumbnail on gallery info
2020-01-15 11:29:58 +09:00
Pupil
e81c189afc Fixed thumbnail on gallery info 2020-01-15 11:28:38 +09:00
Pupil
e0ccac13c1 Forgot to handle error :P 2020-01-15 10:58:53 +09:00
Pupil
93228459d7 Fixed language selection based on locale 2020-01-13 20:53:27 +09:00
Pupil
63e07f56e0 Merge pull request #51 from tom5079/development
Version 4.3
2020-01-13 20:35:43 +09:00
Pupil
ee87122bb2 Fixed Checking permission despite requiring no permission 2020-01-13 20:32:50 +09:00
tom5079
290dda9018 Added log to indicate firebase status 2020-01-13 15:25:44 +09:00
Pupil
1d3d78b936 Merge pull request #50 from tom5079/development
Version 4.2
2020-01-13 15:07:00 +09:00
Pupil
a947bc6415 Merge pull request #49 from tom5079/Pupil-28
Created beta channel update feature
2020-01-13 15:05:58 +09:00
tom5079
9ca891b2f5 Changed SwitchPreference to SwitchPreferenceCompat for better integration 2020-01-13 15:02:00 +09:00
tom5079
48e0ebc8ae Pupil-28 Add option to select update channels 2020-01-13 14:43:55 +09:00
tom5079
b323353006 Merge remote-tracking branch 'origin/development' into development 2020-01-13 14:11:07 +09:00
Pupil
c85d3ebe81 Merge pull request #48 from tom5079/issue-39
Added Changing Download directory feature
2020-01-13 14:09:41 +09:00
tom5079
ce843abec8 Changed logic to update app from utilizing DownloadManager to manual download 2020-01-13 14:08:31 +09:00
tom5079
6b43faa70e Fixed crash when built without google-services.json 2020-01-13 14:08:31 +09:00
tom5079
2d0c997b2e Updated build.gradle 2020-01-13 14:08:31 +09:00
tom5079
1db5118377 Updated .gitignore 2020-01-13 14:08:31 +09:00
tom5079
26b53ed7ac Fixed crash when built without google-services.json 2020-01-12 19:12:47 +09:00
tom5079
2c85ea6443 Removes Permission check for downloading updates
TODO: write logic for downloading update file instead of using DownloadManager(Permission problem)
2020-01-11 18:51:15 +09:00
tom5079
cbc2b30f47 resolves #39 2020-01-11 06:51:51 +09:00
tom5079
0b58deb92c Updated build.gradle 2020-01-04 14:01:54 +09:00
tom5079
ed1cf23c91 Updated .gitignore 2020-01-04 14:00:31 +09:00
tom5079
6fbb644e4b Added download directory entry on preferences
Changed download folder
2020-01-04 13:16:39 +09:00
Pupil
774867502d Merge pull request #47 from tom5079/issue-42
Fixed #42
2020-01-02 10:31:39 +09:00
tom5079
c8b1439aeb Fixed #42 2020-01-02 10:30:55 +09:00
tom5079
38c16adffe Fixed to be able to build without google-services.json 2020-01-02 10:18:39 +09:00
Pupil
18aede2701 Merge pull request #45 from tom5079/issue-44
issue-44
2019-12-29 14:46:13 +09:00
tom5079
c59d08a0a1 Fixes #44 2019-12-29 14:42:28 +09:00
tom5079
66ae29eb5b Fixes #44 2019-12-29 14:24:20 +09:00
tom5079
7d9cb3e150 Dependency update 2019-12-29 13:34:45 +09:00
Pupil
9922a9f82a Merge pull request #41 from tom5079/hotfix-40
Fixes #40
2019-12-19 09:37:23 +09:00
tom5079
445b9b4673 Fixes #40 2019-12-19 09:36:51 +09:00
tom5079
0ef7b358e0 Fixes #40 2019-12-19 09:33:10 +09:00
tom5079
2d3fb75576 Fixes wierd crash 2019-12-14 17:04:43 +09:00
tom5079
d55ff6d68e Fixes wierd crash 2019-12-14 17:04:04 +09:00
Pupil
079654a9c7 Merge pull request #36 from tom5079/Pupil-35
fixes #35
2019-12-14 16:56:58 +09:00
tom5079
30263c6260 fixes #35
warning: this can cause OOM
2019-12-14 16:54:59 +09:00
Pupil
3159c343c1 Merge pull request #34 from tom5079/development
Version 4.2-beta1
2019-12-13 20:10:55 +09:00
tom5079
ceaa930623 bug fix 2019-12-13 20:03:11 +09:00
tom5079
6a8539106b bug fix 2019-12-13 20:01:45 +09:00
tom5079
7a24c3c08e bug fix 2019-12-13 19:50:14 +09:00
tom5079
251abeb090 Merge remote-tracking branch 'origin/development' into development 2019-12-13 19:43:42 +09:00
Pupil
a61fe9f98c Merge pull request #33 from tom5079/Pupil-29
Pupil-29
2019-12-13 19:42:44 +09:00
tom5079
d29c7bf91a Apply update on startup 2019-12-13 19:30:19 +09:00
tom5079
ed4911c441 Updated serialization library 2019-12-13 18:39:12 +09:00
tom5079
d40b4f3748 Added update logic for outdated readers 2019-12-12 20:14:55 +09:00
tom5079
f3c4fe1914 Merge remote-tracking branch 'origin/development' into development 2019-12-11 20:23:45 +09:00
tom5079
55ee841bd0 resolves #31 2019-12-11 20:23:18 +09:00
tom5079
657fb488ee fixed #31 on libpupil
#TODO: fix app side to completely resolve the issue
2019-12-11 20:23:18 +09:00
tom5079
4eef0b93fb bug fix 2019-12-11 20:23:17 +09:00
Pupil
f2be56435c Merge pull request #32 from tom5079/Pupil-31 2019-12-11 20:08:45 +09:00
tom5079
fa6b3ad7ba resolves #31 2019-12-11 20:03:55 +09:00
tom5079
52c05e6888 fixed #31 on libpupil
#TODO: fix app side to completely resolve the issue
2019-12-11 19:52:25 +09:00
tom5079
865bf0ba83 Added code for differentiating readers 2019-12-09 10:33:26 +09:00
tom5079
3f827d1bad bug fix 2019-12-09 10:15:53 +09:00
tom5079
0561d5f55c Added code for saved reader 2019-12-09 09:36:36 +09:00
Pupil
1bf2e1dacc Merge pull request #30 from tom5079/Pupil-24
fixed #24
2019-12-08 18:45:21 +09:00
tom5079
db5a221b56 kotlin plugin update 2019-12-08 18:10:35 +09:00
tom5079
295285f132 Turned off development only option 2019-12-02 19:04:09 +09:00
tom5079
5052b6c074 Removed folder opening feature due to its unstability 2019-12-02 18:55:38 +09:00
tom5079
f98f45dc54 Pupil-24 Absence of backing up favorites feature 2019-12-01 16:58:29 +09:00
tom5079
8d16950f46 Issue #27 fix 2019-11-30 16:10:47 +09:00
tom5079
74033b9f4a Issue #27 fix 2019-11-30 16:10:09 +09:00
tom5079
e497d47374 Fix for bug caused by changed hiyobi domain 2019-11-30 15:10:25 +09:00
tom5079
a97af59260 Potential fix for memory issues 2019-11-30 15:09:53 +09:00
tom5079
2197de98ea Potential fix for too large bitmap crash 2019-11-25 19:39:17 +09:00
tom5079
c004c7f71a Fixed bug fetching old galleries from hiyobi 2019-11-15 19:47:09 +09:00
tom5079
69fc3ad4e8 typo 2019-11-02 21:50:03 +09:00
tom5079
678a8f0914 Merge pull request #21 from tom5079/development
Fixed bug with missing hash
2019-11-02 21:46:11 +09:00
tom5079
08c4c0bf1f Fixed bug with missing hash
Version 4.1
2019-11-02 21:45:27 +09:00
tom5079
f2a2656837 Merge pull request #20 from tom5079/development
Development
2019-11-02 20:30:04 +09:00
tom5079
2011572270 Merge remote-tracking branch 'origin/development' into development 2019-11-02 20:29:24 +09:00
tom5079
3b682667e1 Fixed bug caused by updated hitomi server structure
Version 4.0
2019-11-02 20:25:03 +09:00
tom5079
6da8de6463 Merge pull request #19 from tom5079/master
merge readme
2019-11-02 20:08:28 +09:00
tom5079
039d415871 Update README.md
r/engrish
2019-08-31 23:59:41 +09:00
tom5079
776f53bde0 Update README.md 2019-08-30 22:28:07 +09:00
tom5079
58e535595e Update README.md 2019-08-30 22:27:32 +09:00
tom5079
96ad5f6a6c Update README.md 2019-08-30 22:27:08 +09:00
tom5079
043f7bedd8 Added quick download/delete 2019-08-30 15:24:51 +09:00
tom5079
69bcd8f7c0 Merge pull request #17 from tom5079/development
Version 3.2
2019-08-29 12:11:22 +09:00
tom5079
8a58564812 Version 3.2 2019-08-29 12:10:51 +09:00
tom5079
d346cf431f Merge conflicts 2019-08-29 11:47:01 +09:00
tom5079
c0bce4f3b1 Merge conflicts 2019-08-29 11:43:20 +09:00
tom5079
94d258ddbb Merge conflicts 2019-08-29 11:42:31 +09:00
tom5079
6bdba49284 added missing file 2019-08-29 11:41:08 +09:00
tom5079
9b99baf4bc Added missing files 2019-08-29 11:39:01 +09:00
tom5079
5ad2a538bc Merge pull request #16 from tom5079/master
merge to development branch
2019-08-29 11:29:27 +09:00
tom5079
28703e9bf2 added missing file 2019-08-29 11:25:08 +09:00
tom5079
e664efefe9 Fixed image broken after download finishes 2019-08-28 09:21:36 +09:00
tom5079
27a8694938 Fixed not moving cached gallery to download folder 2019-08-28 09:01:39 +09:00
tom5079
e0a6102d4d Fixed viewer flickering
Added moving with volume button
Added nomedia
Added help link to error snackbar
2019-08-28 08:56:29 +09:00
tom5079
7106cf04ed Delete google-services.json 2019-08-25 09:52:47 +09:00
tom5079
2afdc5591a Added gallery details
Added dark mode
2019-07-21 20:51:50 +09:00
tom5079
8eed4b67c3 Added gallery details
Added dark mode
2019-07-21 20:51:50 +09:00
tom5079
edacef0f2b Update ignore feature added
Bug fixed
2019-07-14 10:53:42 +09:00
tom5079
d28894f8cd Update ignore feature added
Bug fixed
2019-07-14 10:53:42 +09:00
tom5079
ee8e921e1a Image loading optimized
Adds gallery to history when opened directly by gallery ID
Fixed blurred image
2019-07-11 21:24:25 +09:00
tom5079
480d4b1e9a Image loading optimized
Adds gallery to history when opened directly by gallery ID
Fixed blurred image
2019-07-11 21:24:25 +09:00
tom5079
a79c023220 Fixed invisible favorite tag bug 2019-07-07 17:30:03 +09:00
tom5079
efc50df243 Fixed invisible favorite tag bug 2019-07-07 17:30:03 +09:00
tom5079
905ea766b1 App crash fix 2019-07-07 16:36:42 +09:00
tom5079
bce26f4557 App crash fix 2019-07-07 16:36:42 +09:00
tom5079
474d3ad80a Merge pull request #15 from tom5079/development
Version fix
2019-07-07 15:26:57 +09:00
tom5079
a74b2c9b49 Version fix 2019-07-07 15:26:22 +09:00
tom5079
22bdf61bb3 Version fix 2019-07-07 15:26:22 +09:00
tom5079
69f9b099b7 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:25:54 +09:00
tom5079
1d812487a6 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:25:54 +09:00
tom5079
7c2bf8fb9d Version fix 2019-07-07 15:25:36 +09:00
tom5079
dfb78bed69 Version fix 2019-07-07 15:25:36 +09:00
tom5079
fb42b48880 Merge pull request #14 from tom5079/development
Version 3.0
2019-07-07 15:25:02 +09:00
tom5079
bb0988a188 Merge branch 'master' into development 2019-07-07 15:24:53 +09:00
tom5079
c64b6f112b Merge branch 'master' into development 2019-07-07 15:24:53 +09:00
tom5079
9ac7fb490e Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:23:42 +09:00
tom5079
bd88a8a8d3 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:23:42 +09:00
tom5079
1eb75acb40 UI update
Added sort by popularity functionality
Added auto update
2019-07-07 15:21:56 +09:00
tom5079
5ccc96aeb9 UI update
Added sort by popularity functionality
Added auto update
2019-07-07 15:21:56 +09:00
tom5079
ef72d10344 Create README.md 2019-07-03 20:49:34 +09:00
tom5079
573f0b40d1 Create LICENSE 2019-07-03 20:49:13 +09:00
tom5079
48f49edb19 Update LICENSE 2019-07-03 20:46:43 +09:00
tom5079
aa22d9fdd8 Create LICENSE 2019-07-03 20:45:56 +09:00
tom5079
8410a2fdb3 Update LICENSE 2019-07-03 20:44:57 +09:00
tom5079
ec98e4e9a4 Update LICENSE 2019-07-03 20:44:57 +09:00
tom5079
dca6ba457b Added license 2019-07-03 20:44:10 +09:00
tom5079
5b10a781a6 Added license 2019-07-03 20:44:10 +09:00
tom5079
b103188faf Merge pull request #13 from tom5079/master
updated license
2019-07-03 20:07:21 +09:00
tom5079
29637b234c Merge pull request #13 from tom5079/master
updated license
2019-07-03 20:07:21 +09:00
tom5079
34dc238ef1 Create LICENSE 2019-07-03 19:49:40 +09:00
tom5079
3c2675e650 Create README.md 2019-07-03 19:47:13 +09:00
tom5079
7e87bb6838 Search algorithm improved
Language settings in default tag fixed
2019-07-03 19:40:19 +09:00
tom5079
3992a07340 Search algorithm improved
Language settings in default tag fixed
2019-07-03 19:40:19 +09:00
tom5079
bd4b61d7ac Utilizing Glide
Fixed Reader FAB icon
Changed to use gallery id instead of galleryblock to open Reader
2019-06-30 22:04:35 +09:00
tom5079
2046d87031 Utilizing Glide
Fixed Reader FAB icon
Changed to use gallery id instead of galleryblock to open Reader
2019-06-30 22:04:35 +09:00
tom5079
0618d8c6f8 Merge pull request #12 from tom5079/development
Version 2.11.1
2019-06-23 23:33:23 +09:00
tom5079
5bfc27835b Fixed app icon
Version 2.11.1
2019-06-23 23:32:38 +09:00
tom5079
cdc545ea32 Fixed bug for older devices
Hoping that the viewer crashing bug is fixed
Version 2.11
2019-06-23 23:09:01 +09:00
tom5079
449db97a2b Merge pull request #11 from tom5079/development
Pupil v2.10
2019-06-23 16:17:49 +09:00
tom5079
e01380090d Added lock 2019-06-23 10:27:07 +09:00
tom5079
6d1505241e Migrate to Android 29
Re-Added Cache clear to prevent deleting downloading images
2019-06-23 00:23:17 +09:00
tom5079
f303e49e97 Memory optimization 2019-06-14 20:01:32 +09:00
tom5079
0e6b50e302 Merge pull request #10 from tom5079/development
Version 2.9
2019-06-13 22:08:57 +09:00
tom5079
868af1e6a2 Changed to non-scrolling horizontal image view 2019-06-13 21:45:31 +09:00
tom5079
34f7b111ee Bug fix 2019-06-13 21:29:57 +09:00
tom5079
df27907c57 Bug fix 2019-06-13 21:18:26 +09:00
tom5079
75583b9e65 Added URL support
Added Firebase
Added progressbar to the full screen horizontal reader view
2019-06-13 21:05:52 +09:00
123 changed files with 5547 additions and 2025 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@
#Github pages #Github pages
/gh-pages /gh-pages
#Private files
**/google-services.json

View File

@@ -1,18 +1,13 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<AndroidXmlCodeStyleSettings> <AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" /> <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings> </AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>
@@ -23,6 +18,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:android</NAME> <NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -33,6 +29,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:.*</NAME> <NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -44,6 +41,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:id</NAME> <NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -54,6 +52,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:name</NAME> <NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -64,6 +63,7 @@
<match> <match>
<AND> <AND>
<NAME>name</NAME> <NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -74,6 +74,7 @@
<match> <match>
<AND> <AND>
<NAME>style</NAME> <NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -84,6 +85,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -95,6 +97,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -106,6 +109,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE> <XML_NAMESPACE>.*</XML_NAMESPACE>
</AND> </AND>
</match> </match>

6
.idea/copyright/Apache.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Copyright &amp;#36;today.year tom5079&#10;&#10; Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10; you may not use this file except in compliance with the License.&#10; You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10; Unless required by applicable law or agreed to in writing, software&#10; distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10; See the License for the specific language governing permissions and&#10; limitations under the License." />
<option name="myName" value="Apache" />
</copyright>
</component>

6
.idea/copyright/GPL.xml generated Normal file
View File

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

8
.idea/copyright/profiles_settings.xml generated Normal file
View File

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

1
.idea/gradle.xml generated
View File

@@ -13,7 +13,6 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -1,10 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

7
.idea/kotlinCodeInsightSettings.xml generated Normal file
View File

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

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
</project>

5
.idea/misc.xml generated
View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component> </component>
</project> </project>

3
.idea/scopes/Pupil.xml generated Normal file
View File

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

3
.idea/scopes/libpupil.xml generated Normal file
View File

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

2
.idea/vcs.xml generated
View File

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

View File

@@ -1,2 +1,27 @@
# Pupil # Pupil
Hitomi.la viewer for Android
![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true)
*Pupil, Hitomi.la viewer for Android*
# Screenshot
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)
*Main Screen*
![Reader Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/reader-screenshot.png?raw=true)
*Reader Screen*
Images are censored to be SFW
# Installation
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
or Build app yourself
# Manual
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
# Contribution
Any kind of contribution is appriciated. Feel free to leave PR!

View File

@@ -1,10 +1,17 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric' if (file("google-services.json").exists()) {
apply plugin: 'com.google.firebase.firebase-perf' logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.firebase-perf'
} else {
logger.lifecycle("Firebase Disabled")
}
android { android {
compileSdkVersion 29 compileSdkVersion 29
@@ -12,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 20 versionCode 37
versionName "2.11.1" versionName "5.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -23,10 +30,21 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
buildTypes.each {
it.buildConfigField('boolean', 'CENSOR', 'false')
}
} }
kotlinOptions { kotlinOptions {
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental' freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildToolsVersion = '29.0.2'
} }
dependencies { dependencies {
@@ -35,26 +53,36 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0-beta01' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.android.material:material:1.0.0' implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.firebase:firebase-core:17.0.0' implementation 'com.google.android.material:material:1.2.0-alpha04'
implementation 'com.google.firebase:firebase-perf:18.0.1' implementation 'com.google.firebase:firebase-core:17.2.2'
implementation 'com.google.firebase:firebase-perf:19.0.5'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
transitive = false
}
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.gu:option:1.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
testImplementation 'junit:junit:4.12' kapt 'com.github.bumptech.glide:compiler:4.10.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":20,"versionName":"2.11.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":37,"versionName":"5.4","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -1,19 +1,48 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@file:Suppress("UNUSED_VARIABLE")
package xyz.quaver.pupil package xyz.quaver.pupil
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.fetchNozomi
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -29,28 +58,27 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
assertEquals("xyz.quaver.pupil", appContext.packageName) assertEquals("xyz.quaver.pupil", appContext.packageName)
Log.d("Pupil", fetchNozomi().first.size.toString())
} }
@Test @Test
fun checkCacheDir() { fun checkCacheDir() {
val activityTestRule = ActivityTestRule<LockActivity>(LockActivity::class.java) val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
activityTestRule.launchActivity(Intent()) ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file ->
Log.i("PUPILD", "$index: ${file?.absolutePath}")
while(true); }
} }
@Test @Test
fun test_doSearch() { fun test_doSearch() {
val reader = getReader(1426382) val reader = getReader( 1426382)
val data: ByteArray val data: ByteArray
with(URL(reader[0].url).openConnection() as HttpsURLConnection) { with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
@@ -59,4 +87,71 @@ class ExampleInstrumentedTest {
Log.d("Pupil", data.size.toString()) Log.d("Pupil", data.size.toString())
} }
@UseExperimental(ImplicitReflectionSerializer::class)
@Test
fun test_deleteCodeFromReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val json = Json(JsonConfiguration.Stable)
listOf(
getDownloadDirectory(context),
File(context.cacheDir, "imageCache")
).forEach { root ->
root.listFiles()?.forEach gallery@{ gallery ->
val reader = json.parseJson(File(gallery, "reader.json").apply {
if (!exists())
return@gallery
}.readText())
.jsonObject.toMutableMap()
Log.d("PUPILD", gallery.name)
reader.remove("code")
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
}
}
}
@Test
fun test_updateOldReader() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
updateOldReaderGalleries(context)
}
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 515515
val worker = DownloadWorker.getInstance(context)
worker.queue.add(galleryID)
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
if (worker.progress[galleryID]?.all { !it.isFinite() } == true)
break
}
Log.i("PUPILD", "DONE!!")
}
@Test
fun test_getReaderOrNull() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val galleryID = 1561552
runBlocking {
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
}
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
}
} }

View File

@@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil"> package="xyz.quaver.pupil">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="21" />
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -13,8 +18,22 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
<activity android:name=".ui.LockActivity"/> tools:replace="android:theme">
<provider
android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity android:name=".ui.LockActivity" />
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
@@ -37,18 +56,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="히요비.asia" android:host="hiyobi.me"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="xn--9w3b15m8vo.asia"
android:pathPrefix="/reader" android:pathPrefix="/reader"
android:scheme="https" /> android:scheme="https" />
</intent-filter> </intent-filter>
@@ -81,20 +89,9 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="히요비.asia" android:host="hiyobi.me"
android:pathPrefix="/reader" android:scheme="http"
android:scheme="http" /> android:pathPrefix="/reader" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="xn--9w3b15m8vo.asia"
android:pathPrefix="/reader"
android:scheme="http" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -122,6 +119,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
</application> </application>
</manifest> </manifest>

View File

@@ -1,9 +1,28 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil package xyz.quaver.pupil
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -18,7 +37,6 @@ import java.io.File
class Pupil : MultiDexApplication() { class Pupil : MultiDexApplication() {
lateinit var histories: Histories lateinit var histories: Histories
lateinit var downloads: Histories
lateinit var favorites: Histories lateinit var favorites: Histories
init { init {
@@ -29,9 +47,19 @@ class Pupil : MultiDexApplication() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
val download = try {
preference.getString("dl_location", null)
} catch (e: Exception) {
preference.edit().remove("dl_location").apply()
}
if (download == null) {
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
}
try { try {
ProviderInstaller.installIfNeeded(this) ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) { } catch (e: GooglePlayServicesRepairableException) {
@@ -40,21 +68,23 @@ class Pupil : MultiDexApplication() {
e.printStackTrace() e.printStackTrace()
} }
if (!preference.getBoolean("channel_created", false)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply { description = getString(R.string.channel_download_description)
description = getString(R.string.channel_download_description) enableLights(false)
enableLights(false) enableVibration(false)
enableVibration(false) lockscreenVisibility = Notification.VISIBILITY_SECRET
lockscreenVisibility = Notification.VISIBILITY_SECRET
}
manager.createNotificationChannel(channel)
} }
manager.createNotificationChannel(channel)
preference.edit().putBoolean("channel_created", true).apply()
} }
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
super.onCreate() super.onCreate()
} }

View File

@@ -1,43 +1,61 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.app.AlertDialog import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log import android.util.Base64
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.* import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.ReaderItem import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.download.Cache
import java.io.File import xyz.quaver.pupil.util.download.DownloadWorker
import xyz.quaver.pupil.util.wordCapitalize
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -45,10 +63,56 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
PREV PREV
} }
private val glide = Glide.with(context)
private lateinit var favorites: Histories private lateinit var favorites: Histories
inner class GalleryViewHolder(private val view: CardView) : RecyclerView.ViewHolder(view) { val timer = Timer()
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var timerTask: TimerTask? = null
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
val cache = Cache(context).getCachedGallery(galleryID)
val reader = Cache(context).getReaderOrNull(galleryID)
if (reader == null) {
view.galleryblock_progressbar.visibility = View.GONE
view.galleryblock_progress_complete.visibility = View.GONE
return@launch
}
with(view.galleryblock_progressbar) {
progress = cache?.listFiles()?.count { file ->
Regex("^[0-9]+.+\$").matches(file.name!!)
} ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
max = reader.galleryInfo.size
}
if (progress == max) {
if (completeFlag.get(galleryID, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryID, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
}
}
fun bind(galleryBlock: GalleryBlock) {
with(view) { with(view) {
val resources = context.resources val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map { val languages = resources.getStringArray(R.array.languages).map {
@@ -57,89 +121,56 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
}.toMap() }.toMap()
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
val artists = galleryBlock.artists val artists = galleryBlock.artists
val series = galleryBlock.series val series = galleryBlock.series
CoroutineScope(Dispatchers.Default).launch { galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
val cache = thumbnail.await() it.start()
})
if (!File(cache).exists()) CoroutineScope(Dispatchers.Main).launch {
return@launch val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
if (it != null)
val bitmap = BitmapFactory.decodeFile(thumbnail.await()) Base64.decode(it, Base64.DEFAULT)
else
launch(Dispatchers.Main) { null
galleryblock_thumbnail.setImageBitmap(bitmap)
} }
glide
.load(thumbnail)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(galleryblock_thumbnail)
} }
//Check cache //Check cache
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") } val cache = Cache(context).getCachedGallery(galleryBlock.id)
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") } val reader = Cache(context).getReaderOrNull(galleryBlock.id)
if (readerCache.invoke().exists()) { if (cache != null && reader != null) {
val reader = Json(JsonConfiguration.Stable) val count = cache.listFiles().count {
.parse(ReaderItem.serializer().list, readerCache.invoke().readText()) Regex("^[0-9]+.+\$").matches(it.name!!)
}
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.size max = reader.galleryInfo.size
progress = imageCache.invoke().list()?.size ?: 0 progress = count
visibility = View.VISIBLE visibility = View.VISIBLE
} }
} else { } else
galleryblock_progressbar.visibility = View.GONE galleryblock_progressbar.visibility = View.GONE
}
if (refreshTasks[this@GalleryViewHolder] == null) { if (timerTask == null)
val refresh = Timer(false).schedule(0, 1000) { timerTask = timer.schedule(0, 1000) {
post { updateProgress(context, galleryBlock.id)
with(view.galleryblock_progressbar) {
progress = imageCache.invoke().list()?.size ?: 0
if (!readerCache.invoke().exists()) {
visibility = View.GONE
max = 0
progress = 0
view.galleryblock_progress_complete.visibility = View.INVISIBLE
} else {
if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.invoke().readText())
max = reader.size
visibility = View.VISIBLE
}
if (progress == max) {
if (completeFlag.get(galleryBlock.id, false)) {
with(view.galleryblock_progress_complete) {
setImageResource(R.drawable.ic_progressbar)
visibility = View.VISIBLE
}
} else {
with(view.galleryblock_progress_complete) {
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
this?.start()
})
visibility = View.VISIBLE
}
completeFlag.put(galleryBlock.id, true)
}
} else
view.galleryblock_progress_complete.visibility = View.INVISIBLE
null
}
}
}
} }
refreshTasks[this@GalleryViewHolder] = refresh
}
galleryblock_title.text = galleryBlock.title galleryblock_title.text = galleryBlock.title
with(galleryblock_artist) { with(galleryblock_artist) {
text = artists.joinToString(", ") { it.wordCapitalize() } text = artists.joinToString(", ") { it.wordCapitalize() }
@@ -147,19 +178,6 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
artists.isNotEmpty() -> View.VISIBLE artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
if (artists.size > 1) {
AlertDialog.Builder(context).apply {
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, artists)) { _, index ->
for (callback in onChipClickedHandler)
callback.invoke(Tag("artist", artists[index]))
}
}.show()
} else {
for(callback in onChipClickedHandler)
callback.invoke(Tag("artist", artists.first()))
}
}
} }
with(galleryblock_series) { with(galleryblock_series) {
text = text =
@@ -170,31 +188,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
series.isNotEmpty() -> View.VISIBLE series.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
setOnClickListener {
if (series.size > 1) {
AlertDialog.Builder(context).apply {
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, series)) { _, index ->
for (callback in onChipClickedHandler)
callback.invoke(Tag("series", series[index]))
}
}.show()
} else {
for(callback in onChipClickedHandler)
callback.invoke(Tag("series", series.first()))
}
}
}
}
with(galleryblock_type) {
text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
setOnClickListener {
setOnClickListener {
for(callback in onChipClickedHandler)
callback.invoke(Tag("type", galleryBlock.type))
}
}
} }
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(galleryblock_language) { with(galleryblock_language) {
text = text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
@@ -202,48 +197,38 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
galleryBlock.language.isNotEmpty() -> View.VISIBLE galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
setOnClickListener {
for(callback in onChipClickedHandler)
callback.invoke(Tag("language", galleryBlock.language))
}
}
} }
galleryblock_tag_group.removeAllViews() galleryblock_tag_group.removeAllViews()
galleryBlock.relatedTags.forEach { galleryBlock.relatedTags.forEach {
val tag = Tag.parse(it).let { tag -> galleryblock_tag_group.addView(Chip(context).apply {
when { val tag = Tag.parse(it).let { tag ->
tag.area != null -> tag when {
else -> Tag("tag", it) tag.area != null -> tag
else -> Tag("tag", it)
}
} }
}
val chip = LayoutInflater.from(context) chipIcon = when(tag.area) {
.inflate(R.layout.tag_chip, this, false) as Chip "male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
val icon = when(tag.area) { setTextColor(ContextCompat.getColor(context, android.R.color.white))
"male" -> { ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
chip.setChipBackgroundColorResource(R.color.material_blue_700) }
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white)) "female" -> {
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white) setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
}
else -> null
} }
"female" -> { text = tag.tag.wordCapitalize()
chip.setChipBackgroundColorResource(R.color.material_pink_600) setEnsureMinTouchTargetSize(false)
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white)) setOnClickListener {
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white) for (callback in onChipClickedHandler)
callback.invoke(tag)
} }
else -> null })
}
chip.chipIcon = icon
chip.text = tag.tag.wordCapitalize()
chip.setOnClickListener {
for (callback in onChipClickedHandler)
callback.invoke(tag)
}
galleryblock_tag_group.addView(chip)
} }
galleryblock_id.text = galleryBlock.id.toString() galleryblock_id.text = galleryBlock.id.toString()
@@ -295,19 +280,11 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
} }
private fun String.wordCapitalize() : String {
val result = ArrayList<String>()
for (word in this.split(" "))
result.add(word.capitalize())
return result.joinToString(" ")
}
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
val completeFlag = SparseBooleanArray() val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((Int) -> Unit)? = null
var onDeleteClickedHandler: ((Int) -> Unit)? = null
var showNext = false var showNext = false
var showPrev = false var showPrev = false
@@ -333,18 +310,56 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) if (holder is GalleryViewHolder) {
holder.bind(galleries[position-(if (showPrev) 1 else 0)]) val gallery = galleries[position-(if (showPrev) 1 else 0)]
holder.bind(gallery)
with(holder.view.galleryblock_primary) {
setOnClickListener {
holder.view.performClick()
}
setOnLongClickListener {
holder.view.performLongClick()
}
}
holder.view.galleryblock_download.setOnClickListener {
onDownloadClickedHandler?.invoke(position)
}
holder.view.galleryblock_delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position)
}
mItemManger.bindView(holder.view, position)
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text =
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
holder.view.context.getString(R.string.main_download)
else
holder.view.context.getString(android.R.string.cancel)
}
override fun onClose(layout: SwipeLayout?) {}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onOpen(layout: SwipeLayout?) {}
override fun onStartClose(layout: SwipeLayout?) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
})
}
} }
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder) super.onViewDetachedFromWindow(holder)
if (holder is GalleryViewHolder) { if (holder is GalleryViewHolder) {
val task = refreshTasks[holder] ?: return holder.timerTask?.cancel()
holder.timerTask = null
task.cancel()
refreshTasks.remove(holder)
} }
} }
@@ -360,4 +375,6 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
else -> ViewType.GALLERY else -> ViewType.GALLERY
}.ordinal }.ordinal
} }
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
} }

View File

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

View File

@@ -1,73 +1,142 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.graphics.Bitmap import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { class ReaderAdapter(private val context: Context,
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false var isFullScreen = false
var reader: Reader? = null
private val glide = Glide.with(context)
val timer = Timer()
var onItemClickListener : ((Int) -> (Unit))? = null
init {
CoroutineScope(Dispatchers.IO).launch {
reader = Cache(context).getReader(galleryID)
launch(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
LayoutInflater.from(parent.context).inflate( return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false R.layout.item_reader, parent, false
).let { ).let {
return ViewHolder(it) ViewHolder(it)
} }
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ConstraintLayout
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { if (isFullScreen)
// Raw height and width of image holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
val (height: Int, width: Int) = options.run { outHeight to outWidth } else
var inSampleSize = 1 holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
if (height > reqHeight || width > reqWidth) { holder.view.image.setOnPhotoTapListener { _, _, _ ->
onItemClickListener?.invoke(position)
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
} }
with(holder.view as ImageView) { holder.view.container.setOnClickListener {
val options = BitmapFactory.Options() onItemClickListener?.invoke(position)
}
options.inJustDecodeBounds = true (holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
BitmapFactory.decodeFile(images[position], options) .dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
val (reqWidth, reqHeight) = context.resources.displayMetrics.let { holder.view.reader_index.text = (position+1).toString()
Pair(it.widthPixels, it.heightPixels)
val images = Cache(context).getImages(galleryID)
if (images?.get(position) != null) {
glide
.load(images[position]?.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image)
} else {
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (progress?.isNaN() == true) {
if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
glide
.load(R.drawable.image_broken_variant)
.into(holder.view.image)
return
} }
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) holder.view.reader_item_progressbar.progress =
if (progress?.isInfinite() == true)
100
else
progress?.roundToInt() ?: 0
options.inPreferredConfig = Bitmap.Config.RGB_565 holder.view.image.setImageDrawable(null)
options.inJustDecodeBounds = false
val image = BitmapFactory.decodeFile(images[position], options) timer.schedule(1000) {
CoroutineScope(Dispatchers.Main).launch {
setImageBitmap(image) notifyItemChanged(position)
}
}
} }
} }
override fun getItemCount() = images.size override fun getItemCount() = reader?.galleryInfo?.size ?: 0
} }

View File

@@ -0,0 +1,47 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import xyz.quaver.pupil.BuildConfig
class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ImageView(parent.context))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
glide
.load(thumbnails[position])
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view)
}
override fun getItemCount() = thumbnails.size
}

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
@@ -5,7 +23,7 @@ import kotlinx.android.parcel.Parcelize
import xyz.quaver.hitomi.Suggestion import xyz.quaver.hitomi.Suggestion
@Parcelize @Parcelize
data class TagSuggestion constructor(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
override fun getBody(): String { override fun getBody(): String {

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -90,8 +108,8 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
} }
} }
fun removeByArea(area: String) { fun removeByArea(area: String, isNegative: Boolean? = null) {
filter { it.area == area }.forEach { filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
remove(it) remove(it)
} }
} }

View File

@@ -1,6 +1,25 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
@@ -8,6 +27,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_lock.* import kotlinx.android.synthetic.main.activity_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.LockManager
@@ -17,7 +37,18 @@ class LockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock) setContentView(R.layout.activity_lock)
val lockManager = LockManager(this) val lockManager = try {
LockManager(this)
} catch (e: Exception) {
AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
return
}
val mode = intent.getStringExtra("mode") val mode = intent.getStringExtra("mode")
@@ -28,9 +59,10 @@ class LockActivity : AppCompatActivity() {
when(mode) { when(mode) {
null -> { null -> {
if (lockManager.empty()) { if (lockManager.isEmpty()) {
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
return
} }
} }
"add_lock" -> { "add_lock" -> {

View File

@@ -1,15 +1,35 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.* import android.text.*
import android.text.style.AlignmentSpan import android.text.style.AlignmentSpan
import android.view.* import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -17,7 +37,6 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
@@ -27,34 +46,33 @@ import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.android.synthetic.main.dialog_galleryblock.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.content
import kotlinx.serialization.list import kotlinx.serialization.list
import kotlinx.serialization.stringify import kotlinx.serialization.stringify
import ru.noties.markwon.Markwon import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.doSearch
import xyz.quaver.pupil.BuildConfig import xyz.quaver.hitomi.getGalleryIDsFromNozomi
import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.* import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -67,16 +85,24 @@ class MainActivity : AppCompatActivity() {
FAVORITE FAVORITE
} }
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>() enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<GalleryBlock>()
private var query = "" private var query = ""
set(value) { set(value) {
field = value field = value
findViewById<SearchInputView>(R.id.search_bar_text) with(findViewById<SearchInputView>(R.id.search_bar_text)) {
.setText(query, TextView.BufferType.EDITABLE) if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
} }
private var mode = Mode.SEARCH private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
private val REQUEST_SETTINGS = 45162 private val REQUEST_SETTINGS = 45162
private val REQUEST_LOCK = 561 private val REQUEST_LOCK = 561
@@ -87,15 +113,27 @@ class MainActivity : AppCompatActivity() {
private var currentPage = 0 private var currentPage = 0
private lateinit var histories: Histories private lateinit var histories: Histories
private lateinit var downloads: Histories
private lateinit var favorites: Histories private lateinit var favorites: Histories
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK) val lockManager = try {
LockManager(this)
} catch (e: Exception) {
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
checkPermissions() return
}
if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
@@ -113,13 +151,12 @@ class MainActivity : AppCompatActivity() {
with(application as Pupil) { with(application as Pupil) {
this@MainActivity.histories = histories this@MainActivity.histories = histories
this@MainActivity.downloads = downloads
this@MainActivity.favorites = favorites this@MainActivity.favorites = favorites
} }
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
checkUpdate() checkUpdate(this)
initView() initView()
} }
@@ -132,13 +169,19 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
} }
} }
override fun onDestroy() {
super.onDestroy()
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
}
override fun onResume() { override fun onResume() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -155,23 +198,9 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toInt()
val maxPage = Math.ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) {
runOnUiThread {
currentPage++
cancelFetch()
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
}
true
}
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) { if (currentPage > 0) {
runOnUiThread { runOnUiThread {
@@ -179,7 +208,21 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks()
}
}
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) {
runOnUiThread {
currentPage++
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -197,7 +240,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -208,75 +251,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun checkUpdate() {
fun extractReleaseNote(update: JsonObject, locale: String) : String {
val markdown = update["body"]!!.content
val target = when(locale) {
"ko" -> "한국어"
"ja" -> "日本語"
else -> "English"
}
val releaseNote = Regex("^# Release Note.+$")
val language = Regex("^## $target$")
val end = Regex("^#.+$")
var releaseNoteFlag = false
var languageFlag = false
val result = StringBuilder()
for(line in markdown.lines()) {
if (releaseNote.matches(line)) {
releaseNoteFlag = true
continue
}
if (releaseNoteFlag) {
if (language.matches(line)) {
languageFlag = true
continue
}
}
if (languageFlag) {
if (end.matches(line))
break
result.append(line+"\n")
}
}
return getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
}
CoroutineScope(Dispatchers.Default).launch {
val update =
checkUpdate(getString(R.string.release_url), BuildConfig.VERSION_NAME) ?: return@launch
val dialog = AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault().language)
setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ ->
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.update))))
}
setNegativeButton(android.R.string.no) { _, _ ->}
}
launch(Dispatchers.Main) {
dialog.show()
}
}
}
private fun checkPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
}
private fun initView() { private fun initView() {
var prevP1 = 0 var prevP1 = 0
main_appbar_layout.addOnOffsetChangedListener( main_appbar_layout.addOnOffsetChangedListener(
@@ -284,6 +258,13 @@ class MainActivity : AppCompatActivity() {
main_searchview.translationY = p1.toFloat() main_searchview.translationY = p1.toFloat()
main_recyclerview.scrollBy(0, prevP1 - p1) main_recyclerview.scrollBy(0, prevP1 - p1)
with(main_fab) {
if (prevP1 > p1)
hideMenuButton(true)
else if (prevP1 < p1)
showMenuButton(true)
}
prevP1 = p1 prevP1 = p1
} }
) )
@@ -300,7 +281,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.SEARCH mode = Mode.SEARCH
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_history -> { R.id.main_drawer_history -> {
@@ -309,7 +290,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.HISTORY mode = Mode.HISTORY
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_downloads -> { R.id.main_drawer_downloads -> {
@@ -318,7 +299,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.DOWNLOAD mode = Mode.DOWNLOAD
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_favorite -> { R.id.main_drawer_favorite -> {
@@ -327,7 +308,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.FAVORITE mode = Mode.FAVORITE
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
@@ -343,7 +324,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.email))))
} }
R.id.main_drawer_kakaotalk -> { R.id.main_drawer_kakaotalk -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.kakaotalk)))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.discord))))
} }
} }
} }
@@ -351,15 +332,67 @@ class MainActivity : AppCompatActivity() {
true true
} }
with(main_fab_jump) {
setImageResource(R.drawable.ic_jump)
setOnClickListener {
val preference = PreferenceManager.getDefaultSharedPreferences(context)
val perPage = preference.getString("per_page", "25")!!.toInt()
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
runOnUiThread {
cancelFetch()
clearGalleries()
loadBlocks()
}
}
}.show()
}
}
with(main_fab_id) {
setImageResource(R.drawable.numeric)
setOnClickListener {
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ ->
val galleryID = editText.text.toString().toInt()
val intent = Intent(this@MainActivity, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
startActivity(intent)
histories.add(galleryID)
}
}.show()
}
}
setupSearchBar() setupSearchBar()
setupRecyclerView() setupRecyclerView()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply { adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
query = it.toQuery() query = it.toQuery()
@@ -367,85 +400,95 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].id
if (!completeFlag.get(galleryID, false)) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
worker.cancel(galleryID)
else {
Cache(context).setDownloading(galleryID, true)
if (!worker.queue.contains(galleryID))
worker.queue.add(galleryID)
}
}
closeAllItems()
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position].id
CoroutineScope(Dispatchers.Default).launch {
DownloadWorker.getInstance(context).cancel(galleryID)
var cache = Cache(context).getCachedGallery(galleryID)
while (cache != null) {
cache.deleteRecursively()
cache = Cache(context).getCachedGallery(galleryID)
}
histories.remove(galleryID)
if (this@MainActivity.mode != Mode.SEARCH)
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
completeFlag.put(galleryID, false)
}
closeAllItems()
}
} }
ItemClickSupport.addTo(this) ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, v -> .setOnItemClickListener { _, position, v ->
if (v !is CardView) if (v !is CardView)
return@setOnItemClickListener return@setOnItemClickListener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position].first val gallery = galleries[position]
intent.putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery)) intent.putExtra("galleryID", gallery.id)
//TODO: Maybe sprinke some transitions will be nice :D //TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent) startActivity(intent)
histories.add(gallery.id) histories.add(gallery.id)
}.setOnItemLongClickListener { recyclerView, position, v -> }.setOnItemLongClickListener { _, position, v ->
if (v !is CardView) if (v !is CardView)
return@setOnItemLongClickListener true return@setOnItemLongClickListener true
val galleryBlock = galleries[position].first val galleryID = galleries[position].id
val view = LayoutInflater.from(this@MainActivity)
.inflate(R.layout.dialog_galleryblock, recyclerView, false)
val dialog = AlertDialog.Builder(this@MainActivity).apply { GalleryDialog(
setView(view) this@MainActivity,
}.create() galleryID
).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()
currentPage = 0
with(view.main_dialog_download) { cancelFetch()
text = when(GalleryDownloader.get(galleryBlock.id)) { clearGalleries()
null -> getString(R.string.reader_fab_download) fetchGalleries(query, sortMode)
else -> getString(R.string.reader_fab_download_cancel) loadBlocks()
}
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false)
setOnClickListener {
val downloader = GalleryDownloader.get(galleryBlock.id)
if (downloader == null)
GalleryDownloader(context, galleryBlock, true).start()
else {
downloader.cancel()
downloader.clearNotification()
} }
dismiss()
dialog.dismiss()
} }
} }.show()
view.main_dialog_delete.setOnClickListener {
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryBlock.id]) {
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
val data = getCachedGallery(context, galleryBlock.id)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryBlock.id)
if (mode == Mode.DOWNLOAD) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
}
(adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false)
}
dialog.dismiss()
}
dialog.show()
true true
} }
@@ -503,7 +546,6 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query)
loadBlocks() loadBlocks()
} }
@@ -583,7 +625,7 @@ class MainActivity : AppCompatActivity() {
//BOTTOM //BOTTOM
//Scrolling DOWN //Scrolling DOWN
if (dist < 0 && currentPage != Math.ceil(totalItems.toDouble()/perPage).roundToInt()-1) { if (dist < 0 && currentPage != ceil(totalItems.toDouble()/perPage).roundToInt()-1) {
with(main_recyclerview.adapter as GalleryBlockAdapter) { with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(!showNext) { if(!showNext) {
showNext = true showNext = true
@@ -595,13 +637,14 @@ class MainActivity : AppCompatActivity() {
getChildAt(childCount-1) getChildAt(childCount-1)
} }
val absDist = Math.abs(dist) val absDist = abs(dist)
if (next is LinearLayout) { if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next) val icon = next.findViewById<ImageView>(R.id.icon_next)
val text = next.findViewById<TextView>(R.id.text_next).apply { val text = next.findViewById<TextView>(R.id.text_next).apply {
text = getString(R.string.main_move, currentPage+2) text = getString(R.string.main_move, currentPage+2)
} }
if (absDist < 360) { if (absDist < 360) {
next.layoutParams.height = (absDist/2).roundToInt() next.layoutParams.height = (absDist/2).roundToInt()
icon.layoutParams.height = (absDist/2).roundToInt() icon.layoutParams.height = (absDist/2).roundToInt()
@@ -609,8 +652,7 @@ class MainActivity : AppCompatActivity() {
text.layoutParams.width = absDist.roundToInt() text.layoutParams.width = absDist.roundToInt()
target = -1 target = -1
} } else {
else {
next.layoutParams.height = 180 next.layoutParams.height = 180
icon.layoutParams.height = 180 icon.layoutParams.height = 180
icon.rotation = 0f icon.rotation = 0f
@@ -673,9 +715,10 @@ class MainActivity : AppCompatActivity() {
s ?: return s ?: return
if (s.any { it.isUpperCase() }) if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase()) s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
} }
}) })
searchInputView.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
with(main_searchview as FloatingSearchView) { with(main_searchview as FloatingSearchView) {
val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json")
@@ -690,72 +733,52 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_jump -> { R.id.main_menu_sort_newest -> {
val preference = PreferenceManager.getDefaultSharedPreferences(context) sortMode = SortMode.NEWEST
val perPage = preference.getString("per_page", "25")!!.toInt() it.isChecked = true
val editText = EditText(context)
AlertDialog.Builder(context).apply { runOnUiThread {
setView(editText) currentPage = 0
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
Math.ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ -> cancelFetch()
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 clearGalleries()
fetchGalleries(query, sortMode)
runOnUiThread { loadBlocks()
cancelFetch() }
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
}
}.show()
} }
R.id.main_menu_id -> { R.id.main_menu_sort_popular -> {
val editText = EditText(context) sortMode = SortMode.POPULAR
it.isChecked = true
AlertDialog.Builder(context).apply { runOnUiThread {
setView(editText) currentPage = 0
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> cancelFetch()
CoroutineScope(Dispatchers.Default).launch { clearGalleries()
try { fetchGalleries(query, sortMode)
val intent = Intent(this@MainActivity, ReaderActivity::class.java) loadBlocks()
val gallery = }
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
intent.putExtra(
"galleryblock",
Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery)
)
startActivity(intent)
} catch (e: Exception) {
Snackbar.make(main_layout,
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
}
}
}
}.show()
} }
} }
} }
setOnQueryChangeListener { _, query -> setOnQueryChangeListener { _, query ->
clearSuggestions() this@MainActivity.query = query
if (query.isEmpty() or query.endsWith(' '))
return@setOnQueryChangeListener
val currentQuery = query.split(" ").last().replace('_', ' ')
suggestionJob?.cancel() suggestionJob?.cancel()
clearSuggestions()
if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
return@setOnQueryChangeListener
}
val currentQuery = query.split(" ").last().replace('_', ' ')
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }) val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) })
@@ -774,13 +797,14 @@ class MainActivity : AppCompatActivity() {
} }
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ -> setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
val suggestion = item as TagSuggestion item as TagSuggestion
val tag = "${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")}"
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon.setImageDrawable( leftIcon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
resources, resources,
when(suggestion.n) { when(item.n) {
"female" -> R.drawable.ic_gender_female "female" -> R.drawable.ic_gender_female
"male" -> R.drawable.ic_gender_male "male" -> R.drawable.ic_gender_male
"language" -> R.drawable.ic_translate "language" -> R.drawable.ic_translate
@@ -804,8 +828,6 @@ class MainActivity : AppCompatActivity() {
rotation = 0f rotation = 0f
isEnabled = true isEnabled = true
setColorFilter(ContextCompat.getColor(context, R.color.material_orange_500))
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
val favorites = Tags(json.parse(serializer, favoritesFile.readText())) val favorites = Tags(json.parse(serializer, favoritesFile.readText()))
@@ -827,13 +849,13 @@ class MainActivity : AppCompatActivity() {
} }
} }
if (suggestion.t == -1) { if (item.t == -1) {
textView.text = suggestion.s textView.text = item.s
} else { } else {
val text = "${suggestion.s}\n ${suggestion.t}" val text = "${item.s}\n ${item.t}"
val len = text.length val len = text.length
val left = suggestion.s.length val left = item.s.length
textView.text = SpannableString(text).apply { textView.text = SpannableString(text).apply {
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE) val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
@@ -846,14 +868,13 @@ class MainActivity : AppCompatActivity() {
setOnSearchListener(object : FloatingSearchView.OnSearchListener { setOnSearchListener(object : FloatingSearchView.OnSearchListener {
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
val suggestion = searchSuggestion as TagSuggestion if (searchSuggestion !is TagSuggestion)
return
with(searchInputView.text) { with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length) delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")} ") append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
} }
clearSuggestions()
} }
override fun onSearchAction(currentQuery: String?) { override fun onSearchAction(currentQuery: String?) {
@@ -863,7 +884,7 @@ class MainActivity : AppCompatActivity() {
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener { setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (searchInputView.text.isEmpty()) if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map { swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
}) })
@@ -872,18 +893,12 @@ class MainActivity : AppCompatActivity() {
override fun onFocusCleared() { override fun onFocusCleared() {
suggestionJob?.cancel() suggestionJob?.cancel()
val query = searchInputView.text.toString() runOnUiThread {
cancelFetch()
if (query != this@MainActivity.query) { clearGalleries()
this@MainActivity.query = query currentPage = 0
fetchGalleries(query, sortMode)
runOnUiThread { loadBlocks()
cancelFetch()
clearGalleries()
currentPage = 0
fetchGalleries(query)
loadBlocks()
}
} }
} }
}) })
@@ -912,9 +927,8 @@ class MainActivity : AppCompatActivity() {
main_progressbar.show() main_progressbar.show()
} }
private fun fetchGalleries(query: String) { private fun fetchGalleries(query: String, sortMode: SortMode) {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!! val defaultQuery = preference.getString("default_query", "")!!
galleryIDs = null galleryIDs = null
@@ -927,12 +941,14 @@ class MainActivity : AppCompatActivity() {
Mode.SEARCH -> { Mode.SEARCH -> {
when { when {
query.isEmpty() and defaultQuery.isEmpty() -> { query.isEmpty() and defaultQuery.isEmpty() -> {
fetchNozomi(start = currentPage*perPage, count = perPage).let { when(sortMode) {
totalItems = it.second SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
it.first else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply {
totalItems = size
} }
} }
else -> doSearch("$defaultQuery $query").apply { else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
totalItems = size totalItems = size
} }
} }
@@ -953,8 +969,14 @@ class MainActivity : AppCompatActivity() {
} }
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity).listFiles().filter { file ->
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
}.map {
it.name!!.toInt()
}
when { when {
query.isEmpty() -> downloads.toList().apply { query.isEmpty() -> downloads.apply {
totalItems = size totalItems = size
} }
else -> { else -> {
@@ -985,7 +1007,6 @@ class MainActivity : AppCompatActivity() {
private fun loadBlocks() { private fun loadBlocks() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25 val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!!
loadingJob = CoroutineScope(Dispatchers.IO).launch { loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = galleryIDs?.await() val galleryIDs = galleryIDs?.await()
@@ -999,57 +1020,11 @@ class MainActivity : AppCompatActivity() {
return@launch return@launch
} }
when { galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
query.isEmpty() and defaultQuery.isEmpty() and (mode == Mode.SEARCH) ->
galleryIDs
else ->
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size))
}.chunked(5).let { chunks ->
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {
try { Cache(this@MainActivity).getGalleryBlock(galleryID)
val json = Json(JsonConfiguration.Stable)
val serializer = GalleryBlock.serializer()
val galleryBlock =
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
when {
cache.exists() -> json.parse(serializer, cache.readText())
else -> {
getGalleryBlock(galleryID).apply {
this ?: return@apply
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, this))
}
}
}
} ?: return@async null
val thumbnail = async {
val ext = galleryBlock.thumbnails[0].split('.').last()
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
if (!exists())
try {
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
if (this@apply.parentFile?.exists() == false)
this@apply.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(this@apply))
}
} catch (e: Exception) {
delete()
}
}.absolutePath
}
Pair(galleryBlock, thumbnail)
} catch (e: Exception) {
null
}
} }
}.forEach { }.forEach {
val galleryBlock = it.await() val galleryBlock = it.await()

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.content.Intent import android.content.Intent
@@ -7,6 +25,7 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper import androidx.recyclerview.widget.PagerSnapHelper
@@ -15,31 +34,24 @@ import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.* import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.io.IOException
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import xyz.quaver.Code
import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
import java.util.*
import kotlin.concurrent.schedule
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
private val images = ArrayList<String>() private var galleryID = 0
private lateinit var galleryBlock: GalleryBlock
private var gallerySize = 0
private var currentPage = 0 private var currentPage = 0
private var isScroll = true private var isScroll = true
@@ -55,7 +67,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
private lateinit var downloader: GalleryDownloader private val timer = Timer()
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
@@ -66,6 +78,9 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
favorites = (application as Pupil).favorites favorites = (application as Pupil).favorites
window.setFlags( window.setFlags(
@@ -76,22 +91,16 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent) handleIntent(intent)
Crashlytics.setInt("GalleryID", galleryBlock.id) if (Fabric.isInitialized())
Crashlytics.setInt("GalleryID", galleryID)
if (!::galleryBlock.isInitialized) { if (galleryID == 0) {
onBackPressed() onBackPressed()
return return
} }
supportActionBar?.title = galleryBlock.title
supportActionBar?.setDisplayHomeAsUpEnabled(false)
initDownloader()
initView() initView()
initDownloader()
if (!downloader.download)
downloader.start()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -106,25 +115,16 @@ class ReaderActivity : AppCompatActivity() {
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
val nonNumber = Regex("[^-?0-9]+") val nonNumber = Regex("[^-?0-9]+")
val galleryID = when (uri.host) { galleryID = when (uri.host) {
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt() "hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
"히요비.asia" -> lastPathSegment.toInt() "히요비.asia" -> lastPathSegment.toInt()
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt() "xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt() "e-hentai.org" -> uri.pathSegments[1].toInt()
else -> return else -> return
} }
runBlocking {
CoroutineScope(Dispatchers.IO).launch {
galleryBlock = getGalleryBlock(galleryID) ?: return@launch
}.join()
}
} }
} else { } else {
galleryBlock = Json(JsonConfiguration.Stable).parse( galleryID = intent.getIntExtra("galleryID", 0)
GalleryBlock.serializer(),
intent.getStringExtra("galleryblock")!!
)
} }
} }
@@ -148,7 +148,7 @@ class ReaderActivity : AppCompatActivity() {
with(menu?.findItem(R.id.reader_menu_favorite)) { with(menu?.findItem(R.id.reader_menu_favorite)) {
this ?: return@with this ?: return@with
if (favorites.contains(galleryBlock.id)) if (favorites.contains(galleryID))
(icon as Animatable).start() (icon as Animatable).start()
} }
@@ -162,7 +162,7 @@ class ReaderActivity : AppCompatActivity() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue=1
maxValue=gallerySize maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
value=currentPage value=currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -176,7 +176,7 @@ class ReaderActivity : AppCompatActivity() {
dialog.show() dialog.show()
} }
R.id.reader_menu_favorite -> { R.id.reader_menu_favorite -> {
val id = galleryBlock.id val id = galleryID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
if (favorites.contains(id)) { if (favorites.contains(id)) {
@@ -195,8 +195,11 @@ class ReaderActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (::downloader.isInitialized && !downloader.download) timer.cancel()
downloader.cancel() (reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!Cache(this).isDownloading(galleryID))
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -214,121 +217,73 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
//currentPage is 1-based
return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0)
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
true
}
else -> super.onKeyDown(keyCode, event)
}
}
private fun initDownloader() { private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id) val worker = DownloadWorker.getInstance(this).apply {
queue.add(galleryID)
if (d == null) {
try {
d = GalleryDownloader(this, galleryBlock)
} catch (e: IOException) {
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
finish()
return
}
} }
downloader = d.apply { timer.schedule(0, 1000) {
onReaderLoadedHandler = { if (worker.progress.indexOfKey(galleryID) < 0) //loading
CoroutineScope(Dispatchers.Main).launch { return@schedule
with(reader_download_progressbar) {
max = it.size
progress = 0
}
with(reader_progressbar) {
max = it.size
progress = 0
}
gallerySize = it.size if (worker.progress[galleryID] == null) { //Gallery not found
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}" timer.cancel()
} Snackbar
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
.show()
} }
onProgressHandler = {
CoroutineScope(Dispatchers.Main).launch { runOnUiThread {
reader_download_progressbar.progress = it reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0
} reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
}
onDownloadedHandler = { if (title == getString(R.string.reader_loading)) {
val item = it.toList() val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
CoroutineScope(Dispatchers.Main).launch {
if (images.isEmpty()) { if (reader != null) {
images.addAll(item) title = reader.title
reader_recyclerview.adapter?.notifyDataSetChanged() menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
} else {
images.add(item.last()) menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
reader_recyclerview.adapter?.notifyItemInserted(images.size-1) when (reader.code) {
Code.HITOMI -> R.drawable.hitomi
Code.HIYOBI -> R.drawable.ic_hiyobi
else -> android.R.color.transparent
})
} }
} }
}
onErrorHandler = { if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished
if (it is IOException)
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
downloader.download = false
}
onCompleteHandler = {
CoroutineScope(Dispatchers.Main).launch {
reader_download_progressbar.visibility = View.GONE reader_download_progressbar.visibility = View.GONE
animateDownloadFAB(false)
} }
} }
onNotifyChangedHandler = { notify ->
val fab = reader_fab_download
runOnUiThread {
if (notify) {
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
if (downloader.download)
fab.post {
icon.start()
fab.labelText = getString(R.string.reader_fab_download_cancel)
}
else
fab.post {
fab.setImageResource(R.drawable.ic_download)
fab.labelText = getString(R.string.reader_fab_download)
}
}
})
fab.setImageDrawable(icon)
icon?.start()
} else {
runOnUiThread {
fab.setImageResource(R.drawable.ic_download)
}
}
}
}
}
if (downloader.download) {
downloader.invokeOnReaderLoaded()
downloader.invokeOnNotifyChanged()
} }
} }
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(images) adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
onItemClickListener = {
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
if (layoutManager.findFirstVisibleItemPosition() == -1)
return
currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
this@ReaderActivity.reader_progressbar.progress = currentPage
}
})
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, _, _ ->
if (isScroll) { if (isScroll) {
isScroll = false isScroll = false
isFullscreen = true isFullscreen = true
@@ -336,23 +291,54 @@ class ReaderActivity : AppCompatActivity() {
scrollMode(false) scrollMode(false)
fullscreen(true) fullscreen(true)
} else { } else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
} }
} }
}
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy < 0)
this@ReaderActivity.reader_fab.showMenuButton(true)
else if (dy > 0)
this@ReaderActivity.reader_fab.hideMenuButton(true)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
if (layoutManager.findFirstVisibleItemPosition() == -1)
return
currentPage = layoutManager.findFirstVisibleItemPosition()+1
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
this@ReaderActivity.reader_progressbar.progress = currentPage
}
})
} }
reader_fab_fullscreen.setOnClickListener { with(reader_fab_download) {
isFullscreen = true animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
fullscreen(isFullscreen)
reader_fab.close(true) setOnClickListener {
if (Cache(context).isDownloading(galleryID)) {
Cache(context).setDownloading(galleryID, false)
animateDownloadFAB(false)
} else {
Cache(context).setDownloading(galleryID, true)
animateDownloadFAB(true)
}
}
} }
reader_fab_download.setOnClickListener { with(reader_fab_fullscreen) {
downloader.download = !downloader.download setImageResource(R.drawable.ic_fullscreen)
setOnClickListener {
isFullscreen = true
fullscreen(isFullscreen)
if (!downloader.download) this@ReaderActivity.reader_fab.close(true)
downloader.clearNotification() }
} }
} }
@@ -383,4 +369,34 @@ class ReaderActivity : AppCompatActivity() {
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0) (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
} }
private fun animateDownloadFAB(animate: Boolean) {
with(reader_fab_download) {
if (animate) {
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
val worker = DownloadWorker.getInstance(context)
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
post {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
}
else // Or continue animate
post {
icon.start()
labelText = getString(R.string.reader_fab_download_cancel)
}
}
})
setImageDrawable(icon)
icon?.start()
} else {
setImageResource(R.drawable.ic_download)
labelText = getString(R.string.reader_fab_download)
}
}
}
} }

View File

@@ -1,34 +1,52 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_default_query.view.* import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.util.Lock import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.util.LockManager import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.REQUEST_LOCK
import xyz.quaver.pupil.util.REQUEST_RESTORE
import java.io.File import java.io.File
import java.nio.charset.Charset
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
val REQUEST_LOCK = 38238
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -56,325 +74,6 @@ class SettingsActivity : AppCompatActivity() {
super.onResume() super.onResume()
} }
class SettingsFragment : PreferenceFragmentCompat() {
private val suffix = listOf(
"B",
"kB",
"MB",
"GB",
"TB" //really?
)
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
}
private fun getDirSize(dir: File) : String {
var size = dir.walk().map { it.length() }.sum()
var suffixIndex = 0
while (size >= 1024) {
size /= 1024
suffixIndex++
}
return getString(R.string.settings_clear_summary, size, suffix[suffixIndex])
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
with(findPreference<Preference>("app_version")) {
this!!
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = info.versionName
}
with(findPreference<Preference>("delete_cache")) {
this!!
val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("delete_downloads")) {
this!!
val dir = getDownloadDirectory(context)!!
summary = getDirSize(dir)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
val downloads = (activity!!.application as Pupil).downloads
downloads.clear()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("clear_history")) {
this!!
val histories = (activity!!.application as Pupil).histories
summary = getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
histories.clear()
summary = getString(R.string.settings_clear_history_summary, histories.size)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
true
}
}
with(findPreference<Preference>("default_query")) {
this!!
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
summary = preferences.getString("default_query", "") ?: ""
val languages = resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
val excludeBL = "-male:yaoi"
val excludeGuro = listOf("-female:guro", "-male:guro")
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val dialogView = LayoutInflater.from(context).inflate(
R.layout.dialog_default_query,
LinearLayout(context),
false
)
val tags = Tags.parse(
preferences.getString("default_query", "") ?: ""
)
summary = tags.toString()
with(dialogView.default_query_dialog_language_selector) {
adapter =
ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
arrayListOf(
getString(R.string.default_query_dialog_language_selector_none)
).apply {
addAll(languages.values)
}
)
if (tags.any { it.area == "language" }) {
val tag = languages[tags.first { it.area == "language" }.tag]
if (tag != null) {
setSelection(
@Suppress("UNCHECKED_CAST")
(adapter as ArrayAdapter<String>).getPosition(tag)
)
tags.removeByArea("language")
}
}
}
with(dialogView.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL))
tags.remove(excludeBL)
}
with(dialogView.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach {
tags.remove(it)
}
}
with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase())
}
})
}
val dialog = AlertDialog.Builder(context!!).apply {
setView(dialogView)
}.create()
dialogView.default_query_dialog_ok.setOnClickListener {
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
with(dialogView.default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = preferences.getString("default_query", "") ?: ""
tags.clear()
tags.addAll(newTags)
dialog.dismiss()
}
dialog.show()
true
}
}
with(findPreference<Preference>("app_lock")) {
this!!
val lockManager = LockManager(context)
summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
true
}
}
}
}
class LockFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(context!!).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(context!!)
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(context, LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
android.R.id.home -> onBackPressed() android.R.id.home -> onBackPressed()
@@ -383,6 +82,7 @@ class SettingsActivity : AppCompatActivity() {
return true return true
} }
@UseExperimental(ImplicitReflectionSerializer::class)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) { when(requestCode) {
REQUEST_LOCK -> { REQUEST_LOCK -> {
@@ -394,6 +94,62 @@ class SettingsActivity : AppCompatActivity() {
.commitAllowingStateLoss() .commitAllowingStateLoss()
} }
} }
REQUEST_RESTORE -> {
if (resultCode == Activity.RESULT_OK) {
val uri = data?.data ?: return
try {
val json = contentResolver.openInputStream(uri).use { inputStream ->
inputStream!!
inputStream.readBytes().toString(Charset.defaultCharset())
}
(application as Pupil).favorites.addAll(Json.parseList<Int>(json).also {
Snackbar.make(
window.decorView,
getString(R.string.settings_restore_successful, it.size),
Snackbar.LENGTH_LONG
).show()
})
} catch (e: Exception) {
Snackbar.make(
window.decorView,
R.string.settings_restore_failed,
Snackbar.LENGTH_LONG
).show()
}
}
}
REQUEST_DOWNLOAD_FOLDER -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.also { uri ->
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
contentResolver.takePersistableUriPermission(uri, takeFlags)
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", uri.toString())
.apply()
}
}
}
REQUEST_DOWNLOAD_FOLDER_OLD -> {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
if (!File(directory).canWrite())
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString("dl_location", Uri.fromFile(File(directory)).toString())
.apply()
}
}
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@@ -0,0 +1,154 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.dialog_default_query.*
import kotlinx.android.synthetic.main.dialog_default_query.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags
class DefaultQueryDialog(context : Context) : AlertDialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
private val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
private val excludeBL = "-male:yaoi"
private val excludeGuro = listOf("-female:guro", "-male:guro")
private lateinit var dialogView : View
var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null
@SuppressLint("InflateParams")
override fun onCreate(savedInstanceState: Bundle?) {
initDialog()
setTitle(R.string.default_query_dialog_title)
setView(dialogView)
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
with(default_query_dialog_language_selector) {
if (selectedItemPosition != 0)
newTags.add("language:${reverseLanguages[selectedItem]}")
}
if (default_query_dialog_BL_checkbox.isChecked)
newTags.add(excludeBL)
if (default_query_dialog_guro_checkbox.isChecked)
excludeGuro.forEach { tag ->
newTags.add(tag)
}
onPositiveButtonClickListener?.invoke(newTags)
}
super.onCreate(savedInstanceState)
}
@SuppressLint("InflateParams")
private fun initDialog() {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val tags = Tags.parse(
preferences.getString("default_query", "") ?: ""
)
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
with(dialogView.default_query_dialog_language_selector) {
adapter =
ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
arrayListOf(
context.getString(R.string.default_query_dialog_language_selector_none)
).apply {
addAll(languages.values)
}
)
if (tags.any { it.area == "language" && !it.isNegative }) {
val tag = languages[tags.first { it.area == "language" }.tag]
if (tag != null) {
setSelection(
@Suppress("UNCHECKED_CAST")
(adapter as ArrayAdapter<String>).getPosition(tag)
)
tags.removeByArea("language", false)
}
}
}
with(dialogView.default_query_dialog_BL_checkbox) {
isChecked = tags.contains(excludeBL)
if (tags.contains(excludeBL))
tags.remove(excludeBL)
}
with(dialogView.default_query_dialog_guro_checkbox) {
isChecked = excludeGuro.all { tags.contains(it) }
if (excludeGuro.all { tags.contains(it) })
excludeGuro.forEach {
tags.remove(it)
}
}
with(dialogView.default_query_dialog_edittext) {
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
s ?: return
if (s.any { it.isUpperCase() })
s.replace(
0,
s.length,
s.toString().toLowerCase(java.util.Locale.getDefault())
)
}
})
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.*
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.byteToString
@SuppressLint("InflateParams")
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
externalFilesDirs.forEachIndexed { index, dir ->
dir ?: return@forEachIndexed
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
location_type.text = context.getString(when (index) {
0 -> R.string.settings_dl_location_internal
else -> R.string.settings_dl_location_removable
})
location_available.text = context.getString(
R.string.settings_dl_location_available,
byteToString(dir.freeSpace)
)
setOnClickListener {
buttons.forEach { pair ->
pair.first.isChecked = false
}
button.performClick()
preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
}
buttons.add(button to Uri.fromFile(dir))
})
}
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
location_type.text = context.getString(R.string.settings_dl_location_custom)
setOnClickListener {
buttons.forEach { pair ->
pair.first.isChecked = false
}
button.performClick()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
dismiss()
} else { // Can't use SAF on old Androids!
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Pupil")
.allowNewDirectoryNameModification(true)
.build()
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
}
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
dismiss()
}
}
buttons.add(button to null)
})
val pref = Uri.parse(preference.getString("dl_location", null))
val index = externalFilesDirs.indexOfFirst {
Uri.fromFile(it).toString() == pref.toString()
}
if (index < 0)
buttons.last().first.isChecked = true
else
buttons[index].first.isChecked = true
setTitle(R.string.settings_dl_location)
setView(view)
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss()
}
super.onCreate(savedInstanceState)
}
}

View File

@@ -0,0 +1,284 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout.LayoutParams
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_gallery.*
import kotlinx.android.synthetic.main.gallery_details.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailAdapter
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
private val glide = Glide.with(context)
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_gallery)
window?.attributes.apply {
this ?: return@apply
width = LayoutParams.MATCH_PARENT
height = LayoutParams.MATCH_PARENT
}
with(gallery_fab) {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
setOnClickListener {
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
})
(context.applicationContext as Pupil).histories.add(galleryID)
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
val gallery = getGallery(galleryID)
launch(Dispatchers.Main) {
gallery_progressbar.visibility = View.GONE
gallery_title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
with(gallery_type) {
text = gallery.type.wordCapitalize()
setOnClickListener {
gallery.type.let {
when (it) {
"artist CG" -> "artistcg"
"game CG" -> "gamecg"
else -> it
}
}.let {
onChipClickedHandler.forEach { handler ->
handler.invoke(Tag("type", it))
}
}
}
}
Glide.with(context)
.load(gallery.cover)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}.into(gallery_cover)
addDetails(gallery)
addThumbnails(gallery)
addRelated(gallery)
}
} catch (e: Exception) {
Snackbar.make(gallery_layout, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).show()
}
}
}
private fun addDetails(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details)
listOf(
R.string.gallery_artists,
R.string.gallery_groups,
R.string.gallery_language,
R.string.gallery_series,
R.string.gallery_characters,
R.string.gallery_tags
).zip(
listOf(
gallery.artists.map { Tag("artist", it) },
gallery.groups.map { Tag("group", it) },
listOf(gallery.language).map { Tag("language", it) },
gallery.series.map { Tag("series", it) },
gallery.characters.map { Tag("character", it) },
gallery.tags.map {
Tag.parse(it).let { tag ->
when {
tag.area != null -> tag
else -> Tag("tag", it)
}
}
}
)
).filter {
(_, content) -> content.isNotEmpty()
}.forEach { (title, content) ->
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
gallery_details_type.setText(title)
content.forEach { tag ->
gallery_details_tags.addView(
Chip(context).apply {
chipIcon = when(tag.area) {
"male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
}
"female" -> {
setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
}
else -> null
}
text = when (tag.area) {
"language" -> languages[tag.tag]
else -> tag.tag.wordCapitalize()
}
setEnsureMinTouchTargetSize(false)
setOnClickListener {
onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
}
}
}
)
}
}.let {
gallery_details_contents.addView(it)
}
}
}.let {
gallery_contents.addView(it)
}
}
private fun addThumbnails(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails)
RecyclerView(context).apply {
layoutManager = GridLayoutManager(context, 3)
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
}
private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
val galleries = ArrayList<GalleryBlock>()
val adapter = GalleryBlockAdapter(context, galleries).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
}
}
}
CoroutineScope(Dispatchers.Main).launch {
gallery.related.forEachIndexed { i, galleryID ->
async(Dispatchers.IO) {
Cache(context).getGalleryBlock(galleryID)
}.let {
val galleryBlock = it.await() ?: return@let
galleries.add(galleryBlock)
adapter.notifyItemInserted(i)
}
}
}
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related)
RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context)
this.adapter = adapter
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position].id)
})
(context.applicationContext as Pupil).histories.add(galleries[position].id)
}
.setOnItemLongClickListener { _, position, _ ->
GalleryDialog(
context,
galleries[position].id
).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
}
}.show()
true
}
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
}
}

View File

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

View File

@@ -0,0 +1,81 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.fragment
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager
class LockFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("lock_pattern")?.summary =
if (lockManager.contains(Lock.Type.PATTERN))
getString(R.string.settings_lock_enabled)
else
""
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
with(findPreference<Preference>("lock_pattern")) {
this!!
if (LockManager(context!!).contains(Lock.Type.PATTERN))
summary = getString(R.string.settings_lock_enabled)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
val lockManager = LockManager(context!!)
if (lockManager.contains(Lock.Type.PATTERN)) {
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_lock_remove_message)
setPositiveButton(android.R.string.yes) { _, _ ->
lockManager.remove(Lock.Type.PATTERN)
onResume()
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
} else {
val intent = Intent(context, LockActivity::class.java).apply {
putExtra("mode", "add_lock")
putExtra("type", "pattern")
}
startActivity(intent)
}
true
}
}
}
}

View File

@@ -1,4 +1,22 @@
package xyz.quaver.pupil.ui /*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.fragment
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -11,8 +29,6 @@ import com.andrognito.patternlockview.utils.PatternLockUtils
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.* import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.hash
import xyz.quaver.pupil.util.hashWithSalt
class PatternLockFragment : Fragment(), PatternLockViewListener { class PatternLockFragment : Fragment(), PatternLockViewListener {

View File

@@ -0,0 +1,294 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui.fragment
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.ui.dialog.MirrorDialog
import xyz.quaver.pupil.util.*
import java.io.File
class SettingsFragment :
PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
}
override fun onResume() {
super.onResume()
val lockManager = LockManager(context!!)
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when(it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
}
private fun getDirSize(dir: DocumentFile) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
}
override fun onPreferenceClick(preference: Preference?): Boolean {
with (preference) {
this ?: return false
when (key) {
"app_version" -> {
checkUpdate(activity as SettingsActivity, true)
}
"delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_cache_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_downloads_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
if (dir.exists())
dir.deleteRecursively()
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"clear_history" -> {
val histories = (context.applicationContext as Pupil).histories
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
setMessage(R.string.settings_clear_history_alert_message)
setPositiveButton(android.R.string.yes) { _, _ ->
histories.clear()
summary = getString(R.string.settings_clear_history_summary, histories.size)
}
setNegativeButton(android.R.string.no) { _, _ -> }
}.show()
}
"dl_location" -> {
DownloadLocationDialog(activity!!).show()
}
"default_query" -> {
DefaultQueryDialog(context).apply {
onPositiveButtonClickListener = { newTags ->
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
summary = newTags.toString()
}
}.show()
}
"app_lock" -> {
val intent = Intent(context, LockActivity::class.java)
activity?.startActivityForResult(intent, REQUEST_LOCK)
}
"mirrors" -> {
MirrorDialog(context)
.show()
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
context,
getDownloadDirectory(context).let {
if (it.findFile("favorites.json") != null)
it
else
it.createFile("null", "favorites.json")!!
}
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
.show()
}
"restore" -> {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
activity?.startActivityForResult(intent, REQUEST_RESTORE)
}
else -> return false
}
}
return true
}
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
with (preference) {
this ?: return false
when (key) {
"dark_mode" -> {
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
}
else -> return false
}
}
return true
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
"dl_location" -> {
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
initPreferences()
}
private fun initPreferences() {
for (i in 0 until preferenceScreen.preferenceCount) {
preferenceScreen.getPreference(i).run {
if (this is PreferenceCategory)
(0 until preferenceCount).map { getPreference(it) }
else
listOf(this)
}.forEach { preference ->
with (preference) {
when (key) {
"app_version" -> {
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = context.getString(R.string.settings_app_version_description, info.versionName)
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"delete_downloads" -> {
val dir = getDownloadDirectory(context)
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"clear_history" -> {
val histories = (activity!!.application as Pupil).histories
summary = getString(R.string.settings_clear_history_summary, histories.size)
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
summary = getDownloadDirectory(context).uri.path
onPreferenceClickListener = this@SettingsFragment
}
"default_query" -> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
summary = preferences.getString("default_query", "") ?: ""
onPreferenceClickListener = this@SettingsFragment
}
"app_lock" -> {
val lockManager = LockManager(context)
summary =
if (lockManager.locks.isNullOrEmpty()) {
getString(R.string.settings_lock_none)
} else {
lockManager.locks?.joinToString(", ") {
when (it.type) {
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
}
}
}
onPreferenceClickListener = this@SettingsFragment
}
"mirrors" -> {
onPreferenceClickListener = this@SettingsFragment
}
"dark_mode" -> {
onPreferenceChangeListener = this@SettingsFragment
}
"backup" -> {
onPreferenceClickListener = this@SettingsFragment
}
"restore" -> {
onPreferenceClickListener = this@SettingsFragment
}
}
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,178 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util;
/*
* Copyright (C) 2007-2008 OpenIntents.org
*
* 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.
*/
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
/**
* @version 2009-07-03
* @author Peli
* @version 2013-12-11
* @author paulburke (ipaulpro)
*/
public class FileUtils {
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
public static String getPath(final Context context, final Uri uri) {
// DocumentProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}

View File

@@ -1,288 +0,0 @@
package xyz.quaver.pupil.util
import android.app.PendingIntent
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.*
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.R
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList
import kotlin.concurrent.schedule
class GalleryDownloader(
base: Context,
private val galleryBlock: GalleryBlock,
_notify: Boolean = false
) : ContextWrapper(base) {
private val downloads = (applicationContext as Pupil).downloads
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
var download: Boolean = false
set(value) {
if (value) {
field = true
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
val data = getCachedGallery(this, galleryBlock.id)
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
if (File(cache, "images").exists() && !data.exists()) {
cache.copyRecursively(data, true)
cache.deleteRecursively()
}
if (reader?.isActive == false && downloadJob?.isActive != true)
field = false
downloads.add(galleryBlock.id)
} else {
field = false
}
onNotifyChangedHandler?.invoke(value)
}
private val reader: Deferred<Reader>?
private var downloadJob: Job? = null
private lateinit var notificationBuilder: NotificationCompat.Builder
private lateinit var notificationManager: NotificationManagerCompat
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
var onProgressHandler: ((Int) -> Unit)? = null
var onDownloadedHandler: ((List<String>) -> Unit)? = null
var onErrorHandler: ((Exception) -> Unit)? = null
var onCompleteHandler: (() -> Unit)? = null
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
companion object : SparseArray<GalleryDownloader>()
init {
put(galleryBlock.id, this)
initNotification()
reader = CoroutineScope(Dispatchers.IO).async {
download = _notify
val json = Json(JsonConfiguration.Stable)
val serializer = ReaderItem.serializer().list
//Check cache
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "reader.json")
if (cache.exists()) {
val cached = json.parse(serializer, cache.readText())
if (cached.isNotEmpty()) {
useHiyobi = when {
cached.first().url.contains("hitomi.la") -> false
else -> true
}
onReaderLoadedHandler?.invoke(cached)
return@async cached
}
}
//Cache doesn't exist. Load from internet
val reader = when {
useHiyobi -> {
xyz.quaver.hiyobi.getReader(galleryBlock.id).let {
when {
it.isEmpty() -> {
useHiyobi = false
getReader(galleryBlock.id)
}
else -> it
}
}
}
else -> {
getReader(galleryBlock.id)
}
}
if (reader.isNotEmpty()) {
//Save cache
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, reader))
}
reader
}
}
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await()
if (reader.isEmpty())
onErrorHandler?.invoke(IOException("Couldn't retrieve Reader"))
val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader)
notificationBuilder
.setProgress(reader.size, 0, false)
.setContentText("0/${reader.size}")
reader.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it ->
val index = chunkIndex*4+i
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.size, index, false)
.setContentText("$index/${reader.size}")
if (download)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
async(Dispatchers.IO) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
val name = "$index".padStart(4, '0')
val ext = url.split('.').last()
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "images/$name.$ext")
if (!cache.exists())
try {
with(URL(url).openConnection() as HttpsURLConnection) {
if (useHiyobi) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
} else
setRequestProperty("Referer", getReferer(galleryBlock.id))
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(cache))
}
} catch (e: Exception) {
cache.delete()
onErrorHandler?.invoke(e)
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentText(getString(R.string.reader_notification_error))
.setProgress(0, 0, false)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
}
cache.absolutePath
}
}.forEach {
list.add(it.await())
onDownloadedHandler?.invoke(list)
}
}
Timer(false).schedule(1000) {
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
if (download) {
File(cacheDir, "imageCache/${galleryBlock.id}").let {
if (it.exists()) {
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryBlock.id.toString())
if (!target.exists())
target.mkdirs()
it.copyRecursively(target, true)
it.deleteRecursively()
}
}
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
download = false
}
onCompleteHandler?.invoke()
}
remove(galleryBlock.id)
}
}
fun cancel() {
downloadJob?.cancel()
remove(galleryBlock.id)
}
suspend fun cancelAndJoin() {
downloadJob?.cancelAndJoin()
remove(galleryBlock.id)
}
fun invokeOnReaderLoaded() {
CoroutineScope(Dispatchers.Default).launch {
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
}
}
fun clearNotification() {
notificationManager.cancel(galleryBlock.id)
}
fun invokeOnNotifyChanged() {
onNotifyChangedHandler?.invoke(download)
}
private fun initNotification() {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), galleryBlock))
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
setContentTitle(galleryBlock.title)
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_download)
setContentIntent(pendingIntent)
setProgress(0, 0, true)
priority = NotificationCompat.PRIORITY_LOW
}
notificationManager = NotificationManagerCompat.from(this)
}
}

View File

@@ -0,0 +1,237 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.util.*
import java.io.File
import java.net.URL
class Cache(context: Context) : ContextWrapper(context) {
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order
// Download -> Cache
fun getCachedGallery(galleryID: Int) : DocumentFile? {
var file = getDownloadDirectory(this).findFile(galleryID.toString())
if (file?.exists() == true)
return file
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
return if (file.exists())
file
else
null
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
if (file?.exists() != true)
return null
return try {
Json.parse(file.readText(this))
} catch (e: Exception) {
//File corrupted
file.delete()
null
}
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists())
it.mkdirs()
}).createFile("null", ".metadata") ?: return
file.writeText(this, Json.stringify(metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val thumbnail = if (metadata?.thumbnail == null)
withContext(Dispatchers.IO) {
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
try {
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
} catch (e: Exception) {
null
}
}
else
metadata.thumbnail
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
)
return thumbnail
}
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID)
val source = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
val galleryBlock = if (metadata?.galleryBlock == null)
source.entries.map {
CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching {
it.value.invoke()
}.getOrNull()
}
}.firstOrNull {
it.await() != null
}?.await()
else
metadata.galleryBlock
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
)
return galleryBlock
}
fun getReaderOrNull(galleryID: Int): Reader? {
return getCachedMetadata(galleryID)?.reader
}
suspend fun getReader(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
val sources = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
).let {
if (mirrors.isNotEmpty())
it.toSortedMap(
Comparator { o1, o2 ->
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
}
)
else
it
}
val reader = if (metadata?.reader == null) {
CoroutineScope(Dispatchers.IO).async {
var retval: Reader? = null
for (source in sources) {
retval = kotlin.runCatching {
source.value.invoke()
}.getOrNull()
if (retval != null)
break
}
retval
}.await()
} else
metadata.reader
if (reader != null)
setCachedMetadata(
galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
)
return reader
}
fun getImages(galleryID: Int): List<DocumentFile?>? {
val gallery = getCachedGallery(galleryID) ?: return null
val reader = getReaderOrNull(galleryID) ?: return null
val images = gallery.listFiles()
return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name?.startsWith("%05d".format(index)) == true }
}
}
fun putImage(galleryID: Int, name: String, data: ByteArray) {
val cache = getCachedGallery(galleryID) ?:
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
if (!it.exists())
it.mkdirs()
}) ?: return
if (!Regex("""^[0-9]+.+$""").matches(name))
throw IllegalArgumentException("File name is not a number")
cache.let {
if (it.findFile(name) != null)
it
else
it.createFile("null", name)
}?.writeBytes(this, data)
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID)
if (cache != null) {
val download = getDownloadDirectory(this)
if (!download.isParentOf(cache)) {
cache.copyRecursively(this, download)
cache.deleteRecursively()
}
} else
getDownloadDirectory(this).createDirectory(galleryID.toString())
}
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
fun setDownloading(galleryID: Int, isDownloading: Boolean) {
setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
}
}

View File

@@ -0,0 +1,391 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.download
import android.app.PendingIntent
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.SharedPreferences
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.*
import okhttp3.*
import okio.*
import xyz.quaver.Code
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
@UseExperimental(ExperimentalCoroutinesApi::class)
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
//region ProgressListener
@Suppress("UNCHECKED_CAST")
private val progressListener = object: ProgressListener {
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
}
}
interface ProgressListener {
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
}
class ProgressResponseBody(
val tag: Any?,
val responseBody: ResponseBody,
val progressListener : ProgressListener
) : ResponseBody() {
private var bufferedSource : BufferedSource? = null
override fun contentLength() = responseBody.contentLength()
override fun contentType() = responseBody.contentType() ?: null
override fun source(): BufferedSource {
if (bufferedSource == null)
bufferedSource = Okio.buffer(source(responseBody.source()))
return bufferedSource!!
}
private fun source(source: Source) = object: ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
progressListener.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
return bytesRead
}
}
}
//endregion
//region Singleton
companion object {
@Volatile private var instance: DownloadWorker? = null
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: DownloadWorker(context).also { instance = it }
}
}
//endregion
val notificationManager = NotificationManagerCompat.from(context)
val queue = LinkedBlockingQueue<Int>()
/*
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress
* null -> Loading / Gallery doesn't exist
* SECONDARY VALUE
* 0 <= value < 100 -> Download in progress
* Float.POSITIVE_INFINITY -> Download completed
* Float.NaN -> Exception
*/
val progress = SparseArray<MutableList<Float>?>()
/*
* KEY
* primary galleryID
* secondary index
* PRIMARY VALUE
* MutableList -> Download in progress / Loading
* null -> Gallery doesn't exist
* SECONDARY VALUE
* Throwable -> Exception
* null -> Download in progress / Loading
*/
val exception = SparseArray<MutableList<Throwable?>?>()
val notification = SparseArray<NotificationCompat.Builder>()
private val loop = loop()
private val worker = SparseArray<Job?>()
@Volatile var nRunners = 0
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request()
var response = chain.proceed(request)
var retry = preferences.getInt("retry", 3)
while (!response.isSuccessful && retry > 0) {
response = chain.proceed(request)
retry--
}
response.newBuilder()
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build()
}
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.build()
fun stop() {
queue.clear()
loop.cancel()
for (i in 0..worker.size()) {
val galleryID = worker.keyAt(i)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
worker[galleryID]?.cancel()
}
client.dispatcher().cancelAll()
progress.clear()
exception.clear()
notification.clear()
notificationManager.cancelAll()
nRunners = 0
}
fun cancel(galleryID: Int) {
queue.remove(galleryID)
worker[galleryID]?.cancel()
client.dispatcher().queuedCalls()
.filter {
@Suppress("UNCHECKED_CAST")
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
}
.forEach {
it.cancel()
}
progress.remove(galleryID)
exception.remove(galleryID)
notification.remove(galleryID)
notificationManager.cancel(galleryID)
if (progress.indexOfKey(galleryID) >= 0) {
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
}
}
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
val cache = Cache(this@DownloadWorker).getImages(galleryID)
val lowQuality = preferences.getBoolean("low_quality", false)
//Cache exists :P
cache?.get(index)?.let {
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
nRunners--
}
return
}
val request = Request.Builder().apply {
when (reader.code) {
Code.HITOMI -> {
url(
urlFromUrlFromHash(
galleryID,
reader.galleryInfo[index],
if (lowQuality) "webp" else null
)
)
addHeader("Referer", getReferer(galleryID))
}
Code.HIYOBI -> {
url(createImgList(galleryID, reader, lowQuality)[index].path)
addHeader("User-Agent", user_agent)
addHeader("Cookie", cookie)
}
else -> {
//shouldn't be called anyway
}
}
tag(galleryID to index)
}.build()
client.newCall(request).enqueue(callback)
}
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
val reader = Cache(this@DownloadWorker).getReader(galleryID)
//gallery doesn't exist
if (reader == null) {
progress.put(galleryID, null)
exception.put(galleryID, null)
Cache(this@DownloadWorker).setDownloading(galleryID, false)
nRunners--
return@launch
}
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
if (notification[galleryID] == null)
initNotification(galleryID)
notification[galleryID].setContentTitle(reader.title)
notify(galleryID)
for (i in reader.galleryInfo.indices) {
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
if (Fabric.isInitialized())
Crashlytics.logException(e)
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
notify(galleryID)
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
}
nRunners--
}
}
override fun onResponse(call: Call, response: Response) {
response.body().use {
val res = it.bytes()
val ext =
call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
}
notify(galleryID)
if (isCompleted(galleryID)) {
val cache = Cache(this@DownloadWorker)
if (cache.isDownloading(galleryID)) {
cache.moveToDownload(galleryID)
cache.setDownloading(galleryID, false)
}
nRunners--
}
}
}
queueDownload(galleryID, reader, i, callback)
}
}
private fun notify(galleryID: Int) {
val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
if (isCompleted(galleryID))
notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete))
?.setProgress(0, 0, false)
else
notification[galleryID]
?.setProgress(max, progress, false)
?.setContentText("$progress/$max")
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
notificationManager.notify(galleryID, notification[galleryID].build())
else
notificationManager.cancel(galleryID)
}
private fun initNotification(galleryID: Int) {
val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
}
val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
setContentIntent(pendingIntent)
setProgress(0, 0, true)
})
}
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) {
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
continue
val galleryID = queue.poll() ?: continue
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue
initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build())
worker.put(galleryID, download(galleryID))
nRunners++
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2020 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util.download
import kotlinx.serialization.Serializable
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
@Serializable
data class Metadata(
val thumbnail: String? = null,
val galleryBlock: GalleryBlock? = null,
val reader: Reader? = null,
val isDownloading: Boolean? = null
) {
constructor(
metadata: Metadata?,
thumbnail: String? = null,
galleryBlock: GalleryBlock? = null,
readers: Reader? = null,
isDownloading: Boolean? = null
) : this(
thumbnail ?: metadata?.thumbnail,
galleryBlock ?: metadata?.galleryBlock,
readers ?: metadata?.reader,
isDownloading ?: metadata?.isDownloading
)
}

View File

@@ -1,24 +1,151 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import android.os.Build import android.net.Uri
import android.os.Environment import androidx.core.content.FileProvider
import android.provider.MediaStore import androidx.documentfile.provider.DocumentFile
import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager
import java.io.File import java.io.File
import java.net.URL
import java.nio.charset.Charset
import java.util.*
fun getCachedGallery(context: Context, galleryID: Int): File { fun getCachedGallery(context: Context, galleryID: Int) =
return File(getDownloadDirectory(context), galleryID.toString()).let { getDownloadDirectory(context).findFile(galleryID.toString()) ?:
when { DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
it.exists() -> it
else -> File(context.cacheDir, "imageCache/$galleryID") fun getDownloadDirectory(context: Context) : DocumentFile {
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
if (it != null)
Uri.parse(it)
else
Uri.fromFile(context.getExternalFilesDir(null))
}
return if (uri.toString().startsWith("file"))
DocumentFile.fromFile(File(uri.path!!))
else
DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
}
fun convertUpdateUri(context: Context, uri: Uri) : Uri =
if (uri.toString().startsWith("file"))
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
else
uri
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
context.contentResolver.openOutputStream(to.uri).use { out ->
out!!
with(openConnection()) {
val fileSize = contentLength.toLong()
getInputStream().use {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onDownloadProgress?.invoke(bytesCopied, fileSize)
bytes = it.read(buffer)
}
}
}
}
}
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
var parent = file?.parentFile
while (parent != null) {
if (this.uri.path == parent.uri.path)
return true
parent = parent.parentFile
}
return false
}
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
fun DocumentFile.copyRecursively(
context: Context,
target: DocumentFile
) {
if (!exists())
throw Exception("The source file doesn't exist.")
if (this.isFile) {
target.let {
if (it.findFile(name!!) != null)
it
else
createFile("null", name!!)!!
}.writeBytes(
context,
readBytes(context)
)
} else if (this.isDirectory) {
target.createDirectory(name!!).also { newTarget ->
listFiles().forEach { child ->
child.copyRecursively(context, newTarget!!)
}
} }
} }
} }
fun getDownloadDirectory(context: Context): File? { fun DocumentFile.deleteRecursively() {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getExternalFilesDir("Pupil") if (this.isDirectory)
else listFiles().forEach {
File(Environment.getExternalStorageDirectory(), "Pupil") it.deleteRecursively()
}
this.delete()
} }
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
if (state.isEmpty())
state.push(this)
listFiles().forEach {
state.push(it)
if (it.isDirectory) {
it.walk(state)
}
}
return state
}
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
@@ -11,7 +29,7 @@ class Histories(private val file: File) : ArrayList<Int>() {
init { init {
if (!file.exists()) if (!file.exists())
file.parentFile.mkdirs() file.parentFile?.mkdirs()
try { try {
load() load()

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
@@ -94,10 +112,12 @@ class LockManager(base: Context): ContextWrapper(base) {
} }
} }
fun empty(): Boolean { fun isEmpty(): Boolean {
return locks.isNullOrEmpty() return locks.isNullOrEmpty()
} }
fun isNotEmpty(): Boolean = !isEmpty()
fun contains(type: Lock.Type): Boolean { fun contains(type: Lock.Type): Boolean {
return locks?.any { it.type == type } ?: false return locks?.any { it.type == type } ?: false
} }

View File

@@ -0,0 +1,54 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import android.annotation.SuppressLint
import java.util.*
import kotlin.collections.ArrayList
@UseExperimental(ExperimentalStdlibApi::class)
fun String.wordCapitalize() : String {
val result = ArrayList<String>()
@SuppressLint("DefaultLocale")
for (word in this.split(" "))
result.add(word.capitalize(Locale.US))
return result.joinToString(" ")
}
fun byteToString(byte: Long, precision : Int = 1) : String {
val suffix = listOf(
"B",
"kB",
"MB",
"GB",
"TB" //really?
)
var size = byte.toDouble(); var suffixIndex = 0
while (size >= 1024) {
size /= 1024
suffixIndex++
}
return "%.${precision}f ${suffix[suffixIndex]}".format(size)
}

View File

@@ -1,8 +1,42 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.util.Log import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import java.net.URL import java.net.URL
import java.util.*
fun getReleases(url: String) : JsonArray { fun getReleases(url: String) : JsonArray {
return try { return try {
@@ -14,26 +48,160 @@ fun getReleases(url: String) : JsonArray {
} }
} }
fun checkUpdate(url: String, currentVersion: String) : JsonObject? { fun checkUpdate(context: Context, url: String) : JsonObject? {
val releases = getReleases(url) val releases = getReleases(url)
if (releases.isEmpty()) if (releases.isEmpty())
return null return null
val latestVersion = releases[0].jsonObject["tag_name"]?.content return releases.firstOrNull {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
true
else
it.jsonObject["prerelease"]?.boolean == false
}?.let {
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
null
else
it.jsonObject
}
}
return when { fun getApkUrl(releases: JsonObject) : String? {
currentVersion.split('-').size == 1 -> { return releases["assets"]?.jsonArray?.firstOrNull {
when { Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
currentVersion != latestVersion -> releases[0].jsonObject }.let {
else -> null it?.jsonObject?.get("browser_download_url")?.content
}
}
const val UPDATE_NOTIFICATION_ID = 384823
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
if (!force && ignoreUpdateUntil > System.currentTimeMillis())
return
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
val markdown = update["body"]!!.content
val target = when(locale.language) {
"ko" -> "한국어"
"ja" -> "日本語"
else -> "English"
}
val releaseNote = Regex("^# Release Note.+$")
val language = Regex("^## $target$")
val end = Regex("^#.+$")
var releaseNoteFlag = false
var languageFlag = false
val result = StringBuilder()
for(line in markdown.lines()) {
if (releaseNote.matches(line)) {
releaseNoteFlag = true
continue
}
if (releaseNoteFlag) {
if (language.matches(line)) {
languageFlag = true
continue
}
}
if (languageFlag) {
if (end.matches(line))
break
result.append(line+"\n")
} }
} }
else -> {
when { return context.getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
(currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject }
else -> null
CoroutineScope(Dispatchers.Default).launch {
val update =
checkUpdate(context, context.getString(R.string.release_url)) ?: return@launch
val url = getApkUrl(update) ?: return@launch
val dialog = AlertDialog.Builder(context).apply {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault())
setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ ->
val notificationManager = NotificationManagerCompat.from(context)
val builder = NotificationCompat.Builder(context, "download").apply {
setContentTitle(context.getString(R.string.update_notification_description))
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW
}
CoroutineScope(Dispatchers.IO).launch io@{
val target = getDownloadDirectory(context).let {
if (it.findFile("Pupil.apk") != null)
it
else
it.createFile("null", "Pupil.apk")!!
}
try {
URL(url).download(context, target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
} catch (e: Exception) {
builder.apply {
setContentText(context.getString(R.string.update_failed))
setMessage(context.getString(R.string.update_failed_message))
setSmallIcon(android.R.drawable.stat_sys_download_done)
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
return@io
}
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}
builder.apply {
setContentIntent(PendingIntent.getActivity(context, 0, install, 0))
setProgress(0, 0, false)
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentTitle(context.getString(R.string.update_download_completed))
setContentText(context.getString(R.string.update_download_completed_description))
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
context.startActivity(install)
else
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
} }
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
if (!force)
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.apply()
}
}
launch(Dispatchers.Main) {
dialog.show()
} }
} }
} }

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/arrow_right.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
</vector>

View File

@@ -13,14 +13,14 @@
<path <path
android:name="path" android:name="path"
android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"
android:fillColor="#000"/> android:fillColor="@color/material_orange_500"/>
<clip-path <clip-path
android:name="clip" android:name="clip"
android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/> android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
<path <path
android:name="path_1" android:name="path_1"
android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"
android:fillColor="#000"/> android:fillColor="@color/material_orange_500"/>
</vector> </vector>
</aapt:attr> </aapt:attr>
<target android:name="clip"> <target android:name="clip">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#fff" <vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ff000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> <path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector> </vector>

View File

@@ -1,3 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<animated-vector <animated-vector
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"

View File

@@ -1,8 +0,0 @@
<!-- drawable/numeric.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" />
</vector>

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:width="24dp" <vector android:height="24dp" android:width="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"/> <path android:fillColor="@color/material_orange_500" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"/>
</vector> </vector>

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:width="24dp" <vector android:height="24dp" android:width="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"/> <path android:fillColor="@color/material_orange_500" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"/>
</vector> </vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/image_broken_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M21,5V11.59L18,8.58L14,12.59L10,8.59L6,12.59L3,9.58V5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5M18,11.42L21,14.43V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V12.42L6,15.41L10,11.41L14,15.41" />
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/menu.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
</vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" /> <path android:fillColor="#fff" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" />
</vector> </vector>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp">
<shape android:shape="rectangle" >
<stroke
android:width="1dp"
android:color="#555555" />
<solid android:color="@color/transparent" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,8 @@
<!-- drawable/sort_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
</vector>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -11,7 +29,29 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"/> app:layout_constraintBottom_toTopOf="@id/lock_fingerprint_layout"/>
<LinearLayout
android:id="@+id/lock_fingerprint_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="32dp"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/lock_content"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/lock_button_layout" android:id="@+id/lock_button_layout"
@@ -19,7 +59,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="32dp" android:layout_marginBottom="32dp"
app:layout_constraintTop_toBottomOf="@id/lock_content" app:layout_constraintTop_toBottomOf="@id/lock_fingerprint_layout"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"> android:gravity="center">
@@ -41,16 +81,6 @@
app:backgroundTint="@color/dark_gray" app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_password" android:id="@+id/lock_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout <RelativeLayout
android:id="@+id/main_layout" android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
@@ -56,12 +74,37 @@
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.arlib.floatingsearchview.FloatingSearchView <com.arlib.floatingsearchview.FloatingSearchView
android:id="@+id/main_searchview" android:id="@+id/main_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:floatingSearch_backgroundColor="?attr/colorSurface"
app:floatingSearch_searchBarMarginLeft="8dp" app:floatingSearch_searchBarMarginLeft="8dp"
app:floatingSearch_searchBarMarginRight="8dp" app:floatingSearch_searchBarMarginRight="8dp"
app:floatingSearch_searchBarMarginTop="8dp" app:floatingSearch_searchBarMarginTop="8dp"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reader_layout" android:id="@+id/reader_layout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -8,18 +26,11 @@
android:background="@color/dark_gray" android:background="@color/dark_gray"
tools:context=".ui.ReaderActivity"> tools:context=".ui.ReaderActivity">
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:id="@+id/reader_recyclerview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center_vertical"> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reader_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -39,6 +50,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="4dp"
android:progressTint="@color/material_green_a700" android:progressTint="@color/material_green_a700"
tools:ignore="UnusedAttribute"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@@ -55,7 +67,6 @@
android:id="@+id/reader_fab_download" android:id="@+id/reader_fab_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_downloading"
app:fab_label="@string/reader_fab_download" app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/> app:fab_size="mini"/>
@@ -63,7 +74,6 @@
android:id="@+id/reader_fab_fullscreen" android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen" app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/> app:fab_size="mini"/>

View File

@@ -1,20 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"> android:padding="16dp">
<TextView
android:id="@+id/default_query_dialog_title"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/default_query_dialog_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<EditText <EditText
tools:ignore="Autofill" tools:ignore="Autofill"
android:inputType="text" android:inputType="text"
@@ -22,7 +31,7 @@
android:id="@+id/default_query_dialog_edittext" android:id="@+id/default_query_dialog_edittext"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_title" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
@@ -98,14 +107,4 @@
</LinearLayout> </LinearLayout>
<Button
android:id="@+id/default_query_dialog_ok"
style="?borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@android:string/ok"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"/>

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gallery_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/gallery_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageView
android:id="@+id/gallery_cover"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/gallery_title"/>
<TextView
android:id="@+id/gallery_title"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/gallery_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/gallery_title"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<View
android:id="@+id/gallery_padding"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gallery_artist"
app:layout_constraintBottom_toTopOf="@id/gallery_type"/>
<com.google.android.material.chip.Chip
android:id="@+id/gallery_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/gallery_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/gallery_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/gallery_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_anchor="@id/gallery_toolbar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp">
<Button
android:id="@+id/main_dialog_download"
style="?borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/main_dialog_delete"
style="?borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/main_dialog_delete"
app:layout_constraintTop_toBottomOf="@id/main_dialog_download"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -1,10 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.PatternLockFragment"> tools:context=".ui.fragment.PatternLockFragment">
<com.andrognito.patternlockview.PatternLockView <com.andrognito.patternlockview.PatternLockView
android:id="@+id/lock_pattern_view" android:id="@+id/lock_pattern_view"

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/gallery_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"/>
<LinearLayout
android:id="@+id/gallery_details_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true">
<RadioButton
android:id="@+id/button"
android:clickable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/location_type"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/location_available"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
style="@style/TextAppearance.MaterialComponents.Body2"
android:id="@+id/gallery_details_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="8dp"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/gallery_details_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingVertical="4dp"/>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"/>

View File

@@ -1,173 +1,225 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
android:foreground="?attr/selectableItemBackground" android:clipChildren="true">
android:focusable="true"
android:clickable="true">
<LinearLayout <com.daimajia.swipe.SwipeLayout
android:id="@+id/galleryblock_swipe_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> app:drag_edge="right"
app:show_mode="pull_out">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_progress_complete"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View
android:id="@+id/galleryblock_padding"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="@android:color/darker_gray"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/galleryblock_secondary"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:paddingLeft="8dp" android:layout_height="match_parent">
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/galleryblock_id" android:id="@+id/galleryblock_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"/>
<View <TextView
android:layout_width="0dp" android:id="@+id/galleryblock_delete"
android:layout_height="1dp" android:layout_width="wrap_content"
android:layout_weight="1"/> android:layout_height="match_parent"
android:minWidth="70dp"
<ImageView android:padding="8dp"
android:id="@+id/galleryblock_favorite" android:gravity="center"
android:contentDescription="@string/app_name" android:background="@android:color/holo_red_dark"
android:layout_width="32dp" android:textColor="@android:color/white"
android:layout_height="32dp" android:text="@string/main_delete"
app:srcCompat="@drawable/ic_star_empty" android:foreground="?attr/selectableItemBackground"
app:backgroundTint="@color/material_orange_500"/> android:focusable="true"
android:clickable="true"/>
</LinearLayout> </LinearLayout>
</LinearLayout> <LinearLayout
android:id="@+id/galleryblock_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/galleryblock_progressbar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_progress_complete"
android:layout_width="match_parent"
android:layout_height="4dp"
android:visibility="invisible"
android:scaleType="fitXY"
android:contentDescription="@string/reader_imageview_description"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/galleryblock_thumbnail"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:contentDescription="@string/galleryblock_thumbnail_description"
android:adjustViewBounds="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_progressbar"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Headline"
android:id="@+id/galleryblock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/galleryblock_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
<TextView
android:id="@+id/galleryblock_series"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/galleryblock_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<TextView
android:id="@+id/galleryblock_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
<View
android:id="@+id/galleryblock_padding"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_language"
app:layout_constraintBottom_toTopOf="@id/galleryblock_tag_group"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/galleryblock_tag_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
app:chipSpacing="4dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="8dp"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/galleryblock_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<ImageView
android:id="@+id/galleryblock_favorite"
android:contentDescription="@string/app_name"
android:layout_width="32dp"
android:layout_height="32dp"
app:srcCompat="@drawable/ic_star_empty"/>
</LinearLayout>
</LinearLayout>
</com.daimajia.swipe.SwipeLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2020 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingStart="32dp"
android:paddingLeft="32dp"
android:paddingEnd="32dp"
android:paddingRight="32dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/mirror_name"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/mirror_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/menu"
app:tint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

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

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
android:layout_width="match_parent" android:layout_height="wrap_content"> android:layout_width="match_parent" android:layout_height="wrap_content">

View File

@@ -1,8 +1,71 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android" <!--
android:contentDescription="@string/reader_imageview_description" ~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="8dp" xmlns:app="http://schemas.android.com/apk/res-auto">
android:scaleType="fitCenter"
android:adjustViewBounds="true"/> <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/reader_item_boundary">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ProgressBar
android:id="@+id/reader_item_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:progressBarStyleHorizontal"
android:indeterminate="false"
android:progress="0"
android:max="100"
android:visibility="visible"/>
<TextView
android:id="@+id/reader_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Caption"/>
</LinearLayout>
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,7 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ImageView <ImageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height" android:layout_height="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar" android:background="@drawable/side_nav_bar"

View File

@@ -1,3 +1,21 @@
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="24dp"
app:chipIconSize="16dp"
app:chipStartPadding="8dp"
app:chipCornerRadius="100dp"/>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">
@@ -35,7 +53,7 @@
android:title="@string/main_drawer_group_contact_email" android:title="@string/main_drawer_group_contact_email"
android:icon="@drawable/ic_email"/> android:icon="@drawable/ic_email"/>
<item android:id="@+id/main_drawer_kakaotalk" <item android:id="@+id/main_drawer_kakaotalk"
android:title="@string/main_drawer_grouop_contact_kakaotalk" android:title="@string/main_drawer_grouop_contact_discord"
android:icon="@drawable/ic_message"/> android:icon="@drawable/ic_message"/>
</menu> </menu>
</item> </item>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/gallery_favorite"
android:icon="@drawable/ic_star_empty"
android:title=""
app:showAsAction="ifRoom"/>
<item
android:id="@+id/gallery_download"
android:icon="@drawable/ic_download"
android:title=""
app:showAsAction="always"/>
</menu>

View File

@@ -1,16 +1,38 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/main_menu_jump" <item
android:icon="@drawable/ic_jump" android:id="@+id/main_menu_sort"
android:title="@string/main_jump_title" android:title="@string/main_menu_sort">
app:showAsAction="ifRoom"/> <menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_id" <item android:id="@+id/main_menu_sort_newest"
android:icon="@drawable/ic_numeric" android:title="@string/main_menu_sort_newest"
android:title="@string/main_open_gallery_by_id" android:checked="true"/>
app:showAsAction="ifRoom"/> <item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
</group>
</menu>
</item>
<item <item
android:id="@+id/main_menu_settings" android:id="@+id/main_menu_settings"

View File

@@ -1,18 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/reader_menu_favorite" <item android:id="@+id/reader_menu_favorite"
android:title="" android:title=""
android:iconTint="@color/material_orange_500"
android:icon="@drawable/avd_star" android:icon="@drawable/avd_star"
app:showAsAction="always"/> app:showAsAction="always"/>
<item android:id="@+id/reader_menu_use_hiyobi" <item android:id="@+id/reader_type"
android:title="" android:title=""
android:icon="@drawable/ic_hiyobi" app:showAsAction="ifRoom"/>
app:showAsAction="ifRoom"
android:visible="false"/>
<item android:id="@+id/reader_menu_page_indicator" <item android:id="@+id/reader_menu_page_indicator"
android:title="@string/page_indicator_placeholder" android:title="@string/page_indicator_placeholder"

View File

@@ -9,7 +9,7 @@
<string name="search_hint_with_page">ギャラリー検索</string> <string name="search_hint_with_page">ギャラリー検索</string>
<string name="settings_clear_cache">キャッシュクリア</string> <string name="settings_clear_cache">キャッシュクリア</string>
<string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string> <string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
<string name="settings_clear_summary">サイズ: %1$d%2$s</string> <string name="settings_clear_summary">サイズ: %s</string>
<string name="settings_default_query">デフォルトキーワード</string> <string name="settings_default_query">デフォルトキーワード</string>
<string name="settings_galleries_per_page">一回にロードするギャラリー数</string> <string name="settings_galleries_per_page">一回にロードするギャラリー数</string>
<string name="settings_search_title">検索設定</string> <string name="settings_search_title">検索設定</string>
@@ -18,7 +18,7 @@
<string name="update_title">新しいアップデートがあります</string> <string name="update_title">新しいアップデートがあります</string>
<string name="warning">注意</string> <string name="warning">注意</string>
<string name="settings_miscellaneous_title">その他</string> <string name="settings_miscellaneous_title">その他</string>
<string name="settings_use_hiyobi_title">hiyobi.meからロード</string> <string name="settings_mirror_title">ミラーサーバー</string>
<string name="settings_clear_history">履歴を削除</string> <string name="settings_clear_history">履歴を削除</string>
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string> <string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
<string name="settings_clear_history_summary">履歴数: %1$d</string> <string name="settings_clear_history_summary">履歴数: %1$d</string>
@@ -61,16 +61,16 @@
<string name="main_export_error">エクスポートエラーが発生しました</string> <string name="main_export_error">エクスポートエラーが発生しました</string>
<string name="settings_clear_downloads">ダウンロード削除</string> <string name="settings_clear_downloads">ダウンロード削除</string>
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string> <string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか</string>
<string name="settings_use_hiyobi_summary">ロード速度を向上させるため可能であればhiyobi.meからイメージロード</string> <string name="settings_mirror_summary">ミラーサーバからイメージロード</string>
<string name="main_drawer_favorite">お気に入り</string> <string name="main_drawer_favorite">お気に入り</string>
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string> <string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
<string name="main_open_gallery_by_id_error">エラーが発生しました</string> <string name="reader_failed_to_find_gallery">エラーが発生しました</string>
<string name="settings_storage">ストレージ</string> <string name="settings_storage">ストレージ</string>
<string name="main_drawer_grouop_contact_kakaotalk">カカオトーク</string> <string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string> <string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string> <string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン</string> <string name="settings_app_version_title">バージョン(アップデート確認)</string>
<string name="settings_lock_biomatrics">生体認識</string> <string name="settings_lock_biometrics">生体認識</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string> <string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string> <string name="settings_lock_enabled">有効</string>
<string name="settings_lock_fingerprint">指紋</string> <string name="settings_lock_fingerprint">指紋</string>
@@ -79,4 +79,47 @@
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string> <string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="settings_lock_none">なし</string> <string name="settings_lock_none">なし</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string> <string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="reader_loading">ロード中</string>
<string name="main_menu_sort">ソート</string>
<string name="main_menu_sort_newest">投稿日時順</string>
<string name="main_menu_sort_popular">人気順</string>
<string name="update_failed">アップデートに失敗しました</string>
<string name="update_failed_message">アップデート中エラーが発生しました</string>
<string name="ignore_update">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string>
<string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
<string name="gallery_details">ギャラリー情報</string>
<string name="gallery_artists">アーティスト</string>
<string name="gallery_characters">キャラクター</string>
<string name="gallery_groups">グループ</string>
<string name="gallery_language">言語</string>
<string name="gallery_series">シリーズ</string>
<string name="gallery_tags">タグ</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="settings_nomedia_summary">イメージをギャラリーから見えなくする</string>
<string name="settings_nomedia_title">イメージを隠す</string>
<string name="reader_help">ヘルプ</string>
<string name="main_delete">削除</string>
<string name="main_download">ダウンロード</string>
<string name="settings_backup_title">お気に入りバックアップ</string>
<string name="settings_restore_title">お気に入り復元</string>
<string name="settings_backup_snackbar">バックアップファイルを作成しました</string>
<string name="settings_backup_checkout">確認</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_successful">%1$d項目を復元しました</string>
<string name="settings_dl_location">ダウンロード場所</string>
<string name="settings_dl_location_internal">内部ストレージ</string>
<string name="settings_dl_location_removable">外部SDカード</string>
<string name="settings_dl_location_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">低解像度イメージ</string>
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
<string name="settings_dl_location_custom">手動で設定</string>
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
</resources> </resources>

View File

@@ -8,7 +8,7 @@
<string name="settings_default_query">기본 검색어</string> <string name="settings_default_query">기본 검색어</string>
<string name="settings_clear_cache">캐시 정리하기</string> <string name="settings_clear_cache">캐시 정리하기</string>
<string name="settings_clear_cache_alert_message">캐시를 정리하면 이미지 로딩속도가 느려질 수 있습니다. 계속하시겠습니까?</string> <string name="settings_clear_cache_alert_message">캐시를 정리하면 이미지 로딩속도가 느려질 수 있습니다. 계속하시겠습니까?</string>
<string name="settings_clear_summary">사용량: %1$d%2$s</string> <string name="settings_clear_summary">사용량: %s</string>
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string> <string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
<string name="settings_search_title">검색 설정</string> <string name="settings_search_title">검색 설정</string>
<string name="settings_title">설정</string> <string name="settings_title">설정</string>
@@ -18,7 +18,6 @@
<string name="main_no_result">결과 없음</string> <string name="main_no_result">결과 없음</string>
<string name="main_search">검색</string> <string name="main_search">검색</string>
<string name="settings_miscellaneous_title">기타</string> <string name="settings_miscellaneous_title">기타</string>
<string name="settings_use_hiyobi_title">hiyobi.me 사용</string>
<string name="settings_clear_history">기록 삭제</string> <string name="settings_clear_history">기록 삭제</string>
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string> <string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string> <string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
@@ -54,23 +53,22 @@
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string> <string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
<string name="main_move">%1$d 페이지로 이동</string> <string name="main_move">%1$d 페이지로 이동</string>
<string name="https_block_alert_title">접속 불가 현상 안내</string> <string name="https_block_alert_title">접속 불가 현상 안내</string>
<string name="https_block_alert">최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다\n이 경우 플레이스토어에서 SNIper앱을 이용하시면 정상이용이 가능합니다.</string> <string name="https_block_alert">최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다 이 경우 플레이스토어에서 Intra앱을 이용하시면 정상이용이 가능합니다.</string>
<string name="main_dialog_export">갤러리 내보내기</string> <string name="main_dialog_export">갤러리 내보내기</string>
<string name="main_export_complete">내보내기 완료</string> <string name="main_export_complete">내보내기 완료</string>
<string name="main_export_open_folder">폴더 열기</string> <string name="main_export_open_folder">폴더 열기</string>
<string name="main_export_error">내보내기 오류가 발생했습니다</string> <string name="main_export_error">내보내기 오류가 발생했습니다</string>
<string name="settings_clear_downloads">다운로드 삭제</string> <string name="settings_clear_downloads">다운로드 삭제</string>
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string> <string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
<string name="settings_use_hiyobi_summary">속도 향상을 위해 가능하면 hiyobi.me에서 이미지 로드</string>
<string name="main_drawer_favorite">즐겨찾기</string> <string name="main_drawer_favorite">즐겨찾기</string>
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string> <string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
<string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string> <string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
<string name="settings_storage">저장 공간</string> <string name="settings_storage">저장 공간</string>
<string name="main_drawer_grouop_contact_kakaotalk">카카오톡 오픈채팅방</string> <string name="main_drawer_grouop_contact_discord">디스코드</string>
<string name="settings_app_lock">앱 잠금</string> <string name="settings_app_lock">앱 잠금</string>
<string name="settings_app_lock_type">앱 잠금 종류</string> <string name="settings_app_lock_type">앱 잠금 종류</string>
<string name="settings_app_version_title">앱 버전</string> <string name="settings_app_version_title">앱 버전(업데이트 확인)</string>
<string name="settings_lock_biomatrics">생체 인식</string> <string name="settings_lock_biometrics">생체 인식</string>
<string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string> <string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string>
<string name="settings_lock_enabled">사용 중</string> <string name="settings_lock_enabled">사용 중</string>
<string name="settings_lock_fingerprint">지문</string> <string name="settings_lock_fingerprint">지문</string>
@@ -79,4 +77,49 @@
<string name="settings_lock_wrong_confirm">잠금이 일치하지 않습니다. 다시 시도하세요.</string> <string name="settings_lock_wrong_confirm">잠금이 일치하지 않습니다. 다시 시도하세요.</string>
<string name="settings_lock_none">없음</string> <string name="settings_lock_none">없음</string>
<string name="settings_lock_remove_message">잠금을 해제할까요?</string> <string name="settings_lock_remove_message">잠금을 해제할까요?</string>
<string name="reader_loading">로딩중</string>
<string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string>
<string name="update_failed">"업데이트 에러</string>
<string name="update_failed_message">업데이트 중 에러가 발생했습니다</string>
<string name="ignore_update">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string>
<string name="settings_dark_mode_title">다크 모드</string>
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
<string name="gallery_details">갤러리 정보</string>
<string name="gallery_artists">작가</string>
<string name="gallery_characters">캐릭터</string>
<string name="gallery_groups">그룹</string>
<string name="gallery_language">언어</string>
<string name="gallery_series">시리즈</string>
<string name="gallery_tags">태그</string>
<string name="gallery_related">관련 갤러리</string>
<string name="gallery_thumbnails">미리보기</string>
<string name="settings_nomedia_summary">갤러리에서 이미지 검색이 되지 않도록 합니다</string>
<string name="settings_nomedia_title">이미지 숨기기</string>
<string name="reader_help">도움말</string>
<string name="main_delete">삭제</string>
<string name="main_download">다운로드</string>
<string name="settings_backup_title">즐겨찾기 백업</string>
<string name="settings_restore_title">즐겨찾기 복원</string>
<string name="settings_backup_snackbar">백업 파일을 생성하였습니다</string>
<string name="settings_backup_checkout">확인</string>
<string name="settings_restore_failed">복원에 실패했습니다</string>
<string name="settings_restore_successful">%1$d개 항목을 복원했습니다</string>
<string name="settings_dl_location">다운로드 위치</string>
<string name="settings_dl_location_internal">내부 저장공간</string>
<string name="settings_dl_location_removable">외부 SD카드</string>
<string name="settings_dl_location_available">%s 사용 가능</string>
<string name="update_download_completed">다운로드가 완료되었습니다</string>
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
<string name="settings_beta">베타 채널에서 업데이트</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_low_quality">저해상도 이미지</string>
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
<string name="settings_mirror_title">미러 설정</string>
<string name="settings_dl_location_custom">직접 설정</string>
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
</resources> </resources>

View File

@@ -57,4 +57,9 @@
<item>japanese|日本語</item> <item>japanese|日本語</item>
</string-array> </string-array>
<string-array name="mirrors">
<item>HITOMI|hitomi.la</item>
<item>HIYOBI|hiyobi.me</item>
</string-array>
</resources> </resources>

View File

@@ -8,4 +8,6 @@
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen> <dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_height">176dp</dimen>
<dimen name="thumbnail_margin">8dp</dimen>
</resources> </resources>

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