Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac3662a469 | ||
|
|
912e6c5a5b | ||
|
|
6a73a51f63 | ||
|
|
768b6bdda7 | ||
|
|
9ea7cd5f7c | ||
|
|
a91547ace7 | ||
|
|
ed58085b8f | ||
|
|
944e267795 | ||
|
|
325d6df2d3 | ||
|
|
3ba08bf003 | ||
|
|
86d00cc0d1 | ||
|
|
4ee518451c | ||
|
|
7b167b1612 | ||
|
|
7f3965bb08 | ||
|
|
49ea156ea8 | ||
|
|
90c7f9fd98 | ||
|
|
1604885596 | ||
|
|
f0fbf2f46e | ||
|
|
3ab563e25d | ||
|
|
1bdb52a096 | ||
|
|
029401288e | ||
|
|
b84daa45c5 | ||
|
|
ba3fe869d8 | ||
|
|
74494848d3 | ||
|
|
fa0a72e159 | ||
|
|
30c408a27f | ||
|
|
4e9a8be076 | ||
|
|
e3a87d44f4 | ||
|
|
1387b7acf9 | ||
|
|
3df6fed3ac | ||
|
|
30d11e92c6 | ||
|
|
77e5c3a347 | ||
|
|
2ddb26b1aa | ||
|
|
191db14398 | ||
|
|
9dcc24f4d9 | ||
|
|
05d29f4cfd | ||
|
|
f257c936ba | ||
|
|
c2d8cf8b47 | ||
|
|
7cfbfb5db5 | ||
|
|
9d00e47f74 | ||
|
|
452b373f62 | ||
|
|
8fa04644c4 | ||
|
|
67024486e7 | ||
|
|
e1be24053b | ||
|
|
5ddf2b0d8f | ||
|
|
0802b0b4c6 | ||
|
|
2a709e6190 | ||
|
|
156e4f990d | ||
|
|
07c1737597 | ||
|
|
a3f61ab4ef | ||
|
|
7aa3eff23f | ||
|
|
a79375843b | ||
|
|
1b50ce31f5 | ||
|
|
167fe89908 | ||
|
|
892b3ece41 | ||
|
|
6dfa056e41 | ||
|
|
1a8afd86be | ||
|
|
e0ef18659b | ||
|
|
52be469b2d | ||
|
|
6895289ec8 | ||
|
|
3e86cbcf60 | ||
|
|
536d294f7b | ||
|
|
8e7888df35 | ||
|
|
79703ce0dd | ||
|
|
25aa08d8e4 | ||
|
|
35a77cd03d | ||
|
|
1198927d85 | ||
|
|
f1c5e7660e | ||
|
|
0f99403702 | ||
|
|
10170a274b | ||
|
|
47630e6620 | ||
|
|
bb5799b414 | ||
|
|
2110e1b102 | ||
|
|
3dfbd102b4 | ||
|
|
0ba6500625 | ||
|
|
2e0fdcf507 | ||
|
|
b72f5315fe | ||
|
|
2c3bafdac3 | ||
|
|
6d0b55ac26 | ||
|
|
e0bd573988 | ||
|
|
3de3046b8a | ||
|
|
7ccc8dd6b6 | ||
|
|
1d0ae20920 | ||
|
|
cbee05d35b | ||
|
|
ad937488a6 | ||
|
|
9f53f1fe4d | ||
|
|
b33dea3cb0 | ||
|
|
35d7bf6e0b | ||
|
|
9af8c85994 | ||
|
|
ea1233f474 | ||
|
|
a9fe136c55 | ||
|
|
0f5d2abaef | ||
|
|
4ce4983ac8 | ||
|
|
9cc4e31ba1 | ||
|
|
b9fb515832 | ||
|
|
46ad431a5c | ||
|
|
c2e6711575 | ||
|
|
c6d2a61652 | ||
|
|
e3bf0e8341 | ||
|
|
70fa2eb821 | ||
|
|
afe94b5dba | ||
|
|
846f83918b | ||
|
|
27caf83ef3 | ||
|
|
24761fe05b | ||
|
|
3726530af9 | ||
|
|
8e0f5dc697 | ||
|
|
24d6b47941 | ||
|
|
7fd3ae0702 | ||
|
|
10caf7bbd8 | ||
|
|
3ee63bdd47 | ||
|
|
2e4f6398f6 | ||
|
|
542eeda11c | ||
|
|
0ced054631 | ||
|
|
2e74222244 | ||
|
|
9f23e4d7b3 | ||
|
|
05abc83e6e | ||
|
|
18ea016014 | ||
|
|
b04264793b | ||
|
|
cb76649fdf | ||
|
|
b19c9fb132 | ||
|
|
aa759f5187 | ||
|
|
ef504d5a71 | ||
|
|
8cfa98e971 | ||
|
|
4577168953 | ||
|
|
ac74a30fcf | ||
|
|
ca802940b4 | ||
|
|
923858caa9 | ||
|
|
68ae37093a | ||
|
|
000aeb920e | ||
|
|
ce8da252c3 | ||
|
|
de6a664729 | ||
|
|
12bf20f656 | ||
|
|
c58493726e | ||
|
|
541e637df0 | ||
|
|
9d570a91d2 | ||
|
|
d54fe52060 | ||
|
|
1eb86355cb | ||
|
|
e48ecac3cd | ||
|
|
850a8fb35c | ||
|
|
edd1ec2cc1 | ||
|
|
622d4055d9 | ||
|
|
36aa37359b | ||
|
|
7c44cd3ebb | ||
|
|
95a3581ce6 | ||
|
|
41b27de058 | ||
|
|
0db262681b | ||
|
|
b75c74096c | ||
|
|
c68c67b192 | ||
|
|
9ffd0971ae | ||
|
|
b276f2f25f | ||
|
|
e36c77c450 | ||
|
|
4c8cb4eb97 | ||
|
|
746abe91ec | ||
|
|
df0dee36c2 | ||
|
|
5af07c5f07 | ||
|
|
ed5dd9af21 | ||
|
|
d1cfafa944 | ||
|
|
b4428dc1dc | ||
|
|
f649687b74 | ||
|
|
905f327031 | ||
|
|
12f857a936 | ||
|
|
01daf4a63e | ||
|
|
cf3769959f | ||
|
|
10e853f968 | ||
|
|
ac6995be8d | ||
|
|
045d6f15a7 | ||
|
|
2d16f1cb57 | ||
|
|
ebe1d451b0 | ||
|
|
7faa4bea0d | ||
|
|
3785d3130c | ||
|
|
9daa1c27c4 | ||
|
|
67c722ed43 | ||
|
|
3c8f1809aa | ||
|
|
c5a3059e31 | ||
|
|
f305a59880 | ||
|
|
aa18e5f2f1 | ||
|
|
ac25b95eae | ||
|
|
a92fa304f1 | ||
|
|
2f3f3d6480 | ||
|
|
0d108393fc | ||
|
|
8d02f8c6a3 | ||
|
|
87d284ad97 | ||
|
|
c7ffdbec93 | ||
|
|
f9aad6ce9c | ||
|
|
7a64fd4c11 | ||
|
|
909af0cbed | ||
|
|
34db6fc79f | ||
|
|
b025f150c9 | ||
|
|
fea6b38c1e | ||
|
|
e2a03427f9 | ||
|
|
654cb5fdd5 | ||
|
|
333419b716 | ||
|
|
c66b1e4622 | ||
|
|
ac55e2f8fc | ||
|
|
24718e7d25 | ||
|
|
72e1fe9621 | ||
|
|
0c7bf96bf5 | ||
|
|
f1dde6c8bc | ||
|
|
3177a8352f | ||
|
|
3cb024b7f5 | ||
|
|
0a09d16500 | ||
|
|
a79b1ad785 | ||
|
|
4388c53929 | ||
|
|
02f000d37b | ||
|
|
7259e5e315 | ||
|
|
7ce744b5af | ||
|
|
57f21c8d39 | ||
|
|
07f27a8438 | ||
|
|
4f4dc55b4e | ||
|
|
8e9cda8e93 | ||
|
|
d5cfabd73e | ||
|
|
4f1b371e75 | ||
|
|
4d8b50912d | ||
|
|
660c717cfb | ||
|
|
70d3d9a5b2 | ||
|
|
464da25ca1 | ||
|
|
c2795120e1 | ||
|
|
b10fb46f1f | ||
|
|
e39897176a | ||
|
|
6e7e6fab74 | ||
|
|
94009b6829 | ||
|
|
289217fefc | ||
|
|
97feab1203 | ||
|
|
a9a3b20eb3 | ||
|
|
7af1e168a4 | ||
|
|
c8bf34e0d7 | ||
|
|
86d2282cd6 | ||
|
|
cfdb23d6dc | ||
|
|
1874ddd15b | ||
|
|
a004c308cc | ||
|
|
863337c789 | ||
|
|
64d91eed33 | ||
|
|
d0c3a3e14d | ||
|
|
464636d87d | ||
|
|
0b0a1f456f | ||
|
|
302030ca65 | ||
|
|
3e63738542 | ||
|
|
d6823c9c8e | ||
|
|
6bc3866660 | ||
|
|
f41f4dd8ba | ||
|
|
cde39812ce | ||
|
|
03c2abc901 | ||
|
|
7646334525 | ||
|
|
f8d1cd3fab | ||
|
|
4ea94be64a | ||
|
|
dc0470e75a | ||
|
|
5d49ecdd7d | ||
|
|
ef2441116d | ||
|
|
207909c7be | ||
|
|
ddb3dd2d95 | ||
|
|
8bb980fe97 | ||
|
|
3c3a201ed8 | ||
|
|
81bb37e929 | ||
|
|
ae34690eb2 | ||
|
|
285b0f5a69 | ||
|
|
a5327fce07 | ||
|
|
a4b750f9cd | ||
|
|
943344d4bb | ||
|
|
79f1b92960 | ||
|
|
575ae38a28 | ||
|
|
f90e2a5c80 | ||
|
|
4d9b2877fe | ||
|
|
4a955947a2 | ||
|
|
7307c136ee | ||
|
|
657e1b1d0f | ||
|
|
43edf11275 | ||
|
|
32ec4c6715 | ||
|
|
84ac57d69e | ||
|
|
328e7745b7 | ||
|
|
ccf22eaef4 | ||
|
|
c89d42f368 | ||
|
|
41192ea7f5 | ||
|
|
7840987a7c | ||
|
|
fcbe0a7f55 | ||
|
|
f12894c95d | ||
|
|
40f139fda2 | ||
|
|
b8507a371e | ||
|
|
e5a71ce906 | ||
|
|
f9e708fe1e | ||
|
|
50ba6f2bd2 | ||
|
|
2484371f8c | ||
|
|
ba675c4212 | ||
|
|
6f9bc3135a | ||
|
|
4475d4d018 | ||
|
|
08e45db49d | ||
|
|
85c76065a5 | ||
|
|
13582c3f1b | ||
|
|
ae941a4955 | ||
|
|
72da7ebfe4 | ||
|
|
8e1bacbfac | ||
|
|
747c19c028 | ||
|
|
aca83fc1ee | ||
|
|
35babd089d | ||
|
|
457ea56aab | ||
|
|
ed22de2360 | ||
|
|
8d386b44bc | ||
|
|
c4c88c40c5 | ||
|
|
e637ad6acb | ||
|
|
3b9e08e1c7 | ||
|
|
4a2aa8a975 | ||
|
|
9c08eda38a | ||
|
|
713d972d16 | ||
|
|
d4b3705a9e | ||
|
|
c543d1540c | ||
|
|
2956ff08bd | ||
|
|
ff09dec4e4 | ||
|
|
5e02479aa5 | ||
|
|
6157b9cf82 | ||
|
|
9260633e26 | ||
|
|
1a2ae6b1a8 | ||
|
|
282b4adaff | ||
|
|
8519b5a096 | ||
|
|
4a00be922e | ||
|
|
31f30cf1b1 | ||
|
|
62737cc32f | ||
|
|
c7306b2358 | ||
|
|
779958dfeb | ||
|
|
d0ca6872b0 | ||
|
|
161f191f3d | ||
|
|
2cfa46ed05 | ||
|
|
005e2b3fa1 | ||
|
|
3826dfb1d4 | ||
|
|
81bb9a852f | ||
|
|
37e60673d7 | ||
|
|
9b618f7ecc | ||
|
|
0ce3ceb900 | ||
|
|
4833226241 | ||
|
|
5cae964a1e | ||
|
|
70eaa35bc9 | ||
|
|
38bd3fede1 | ||
|
|
e1e32a7f5e | ||
|
|
b77d0fb47a | ||
|
|
4076b2f900 | ||
|
|
1bc409b54a | ||
|
|
1247a0ade9 | ||
|
|
2fd688ff9e | ||
|
|
b0274946bf | ||
|
|
b78c132742 | ||
|
|
b33c46de82 | ||
|
|
25c8bba1b0 | ||
|
|
089f5cb7ec | ||
|
|
95a6a9dca6 | ||
|
|
e19759a7e4 | ||
|
|
8f95ffeebf | ||
|
|
98f08feb16 | ||
|
|
50baaffc36 | ||
|
|
66e9f43bba | ||
|
|
0cb13bb6a3 | ||
|
|
4278bf4631 | ||
|
|
7fa2dd1b74 | ||
|
|
afcd5b0d6e | ||
|
|
2f49748a3b | ||
|
|
4dffa25819 | ||
|
|
fcce3a04a6 | ||
|
|
bc38c8c135 | ||
|
|
4e462a1903 | ||
|
|
43bc5ab50c | ||
|
|
fed04784db | ||
|
|
4dbf5a45e8 | ||
|
|
5d22e70ded | ||
|
|
a3cae20478 | ||
|
|
f7cfbf1b34 | ||
|
|
4d4358f806 | ||
|
|
6ca3c3261e | ||
|
|
635a0b3af1 | ||
|
|
4288bc790d | ||
|
|
c52c91e1c0 |
No files matched your search
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: 功能申请 / Feature request
|
||||
about: 有个想法 / Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**这个功能是用来解决问题的吗? / Is your feature request related to a problem? Please describe.**
|
||||
描述这个问题。
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**描述你想要的解决方法 / Describe the solution you'd like**
|
||||
描述应通过何种行为来解决这个问题。
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**备注 / Additional context**
|
||||
还有别的想说的吗?
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug 反馈 / Bug report
|
||||
about: 出问题了 / Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**简略描述 / Describe the bug**
|
||||
大概说下是什么问题。
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**如何重现 / To Reproduce**
|
||||
重现的步骤:
|
||||
1. 打开什么什么
|
||||
2. 点击什么什么
|
||||
3. 出现什么什么问题
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**预期行为 / Expected behavior**
|
||||
应该是什么样子的?
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**截图 / Screenshots**
|
||||
能截图的话就发截图,请注意风序良俗。
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**设备型号与 Android 版本 / Device model and Android version**
|
||||
- 设备型号:【比如 Pixel 6 XXXL】
|
||||
- Android 版本:【比如 Android 12.1】
|
||||
- Device model: [e.g. Pixel 6 XXXL]
|
||||
- Android version: [e.g. Android 12.1]
|
||||
|
||||
**备注 / Additional context**
|
||||
还有什么想说的?
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,122 @@
|
||||
name: Bug 反馈 / Bug report
|
||||
description: 提交一个问题报告 / Create a bug report to help us improve
|
||||
labels:
|
||||
- "bug"
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤 / Step to reproduce
|
||||
description: |
|
||||
在此处写下复现的方式
|
||||
Describe how to reproduce here
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id : expected
|
||||
attributes:
|
||||
label: 预期行为 / Expected behaviour
|
||||
description: |
|
||||
在此处说明正常情况下应用的预期行为
|
||||
Describe what should happened here
|
||||
placeholder: |
|
||||
它应该XXX...
|
||||
It should be ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际行为 / Actual Behaviour
|
||||
description: |
|
||||
在此处描绘应用的实际行为,最好附上截图
|
||||
Describe what actually happened here, screenshots is better
|
||||
placeholder: |
|
||||
实际上它XXX...
|
||||
Actually it ...
|
||||
[截图] / [Screenshots]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: Android 系统版本 / Android version
|
||||
description: Android 分支名称 + 版本号 / AOSP fork name + version code
|
||||
placeholder: MIUI 12.5, ArrowOS 12.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: 设备型号 / Device model
|
||||
description: 在此填入设备型号 / Put device model here
|
||||
placeholder: OnePlus 7 Pro, Xiaomi 12 Ultra
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: SoC
|
||||
attributes:
|
||||
label: SoC 型号 / Soc model
|
||||
description: 在此填入 SoC 型号 / Put Soc model here
|
||||
placeholder: 骁龙 8+ Gen 1, Snapdragon 8+ Gen 1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: 应用日志 / App logs
|
||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: check
|
||||
attributes:
|
||||
label: 自查步骤 / Verify steps
|
||||
description: |
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
|
||||
required: true
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
|
||||
required: true
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: 所有其他问题 / All other questions
|
||||
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
|
||||
about: 转到 Discussions / Turn to discussions
|
||||
@@ -0,0 +1,85 @@
|
||||
name: 功能请求 / Feature request
|
||||
description: 提出一个功能建议 / Suggest an idea for this project
|
||||
labels:
|
||||
- "enhancement"
|
||||
body:
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: 需求 / Requirement
|
||||
description: |
|
||||
在此处描述您的需求。这通常会是一个您想要的功能
|
||||
Ddescribe what you need here.
|
||||
placeholder: |
|
||||
我需要 XXX 功能 ...
|
||||
I want ABC feature ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: impl
|
||||
attributes:
|
||||
label: 建议实现 / Suggested implements
|
||||
description: |
|
||||
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
|
||||
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
|
||||
placeholder: |
|
||||
建议在 XX 处添加 XX ...
|
||||
I recommend add ABC feature to DEF...
|
||||
图片(如果有)/ Photos (if exists)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: more
|
||||
attributes:
|
||||
label: 其他细节 / Addition details
|
||||
description: |
|
||||
在此处写下其他您想说的内容
|
||||
Describe addition details here
|
||||
placeholder: |
|
||||
其他有用的信息与附件
|
||||
Additional details and attachments
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: check
|
||||
attributes:
|
||||
label: 自查步骤 / Verify steps
|
||||
description: |
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
|
||||
required: true
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
|
||||
required: true
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Sad Panda / 下载太慢 / Slow download speed
|
||||
about: 所有其他问题 / All other questions
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
加入我们的 Telegram 群组,[https://t.me/ehviewer](https://t.me/ehviewer)。温柔[大小]姐姐手把手帮你解决问题。
|
||||
Join our Telegram group, [https://t.me/ehviewer](https://t.me/ehviewer). Administrators will help you solve the problem.
|
||||
@@ -1,57 +0,0 @@
|
||||
name: EhViewer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build
|
||||
env:
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
ALIAS_NAME: ${{ secrets.ALIAS_NAME }}
|
||||
ALIAS_PASS: ${{ secrets.ALIAS_PASS }}
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
echo ::set-env name=APK_FILE::$(find app/build/outputs/apk -name "*.apk")
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
tag_name: 1.7.4_${{ github.run_id }}
|
||||
release_name: 1.7.4_${{ github.run_id }}
|
||||
body: ""
|
||||
draft: false
|
||||
prerelease: true
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ${{ env.APK_FILE }}
|
||||
asset_name: app-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload artifact
|
||||
with:
|
||||
name: EhViewer
|
||||
path: app/build/outputs/
|
||||
@@ -0,0 +1,54 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Update x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Update ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Update x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Pull Request Check
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Update x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Update ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Update x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
body: Bump Version
|
||||
discussion_category_name: Announcements
|
||||
files: |
|
||||
app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
app/build/outputs/apk/release/app-x86-release.apk
|
||||
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
+9
-6
@@ -1,12 +1,15 @@
|
||||
[submodule "native/src/main/jni/a7zip/p7zip/p7zip"]
|
||||
path = native/src/main/jni/a7zip/p7zip/p7zip
|
||||
url = https://github.com/seven332/p7zip.git
|
||||
[submodule "native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo"]
|
||||
path = native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo
|
||||
url = https://gitlab.com/NekoInverter/libjpeg-turbo.git
|
||||
url = https://github.com/Ehviewer-Overhauled/libjpeg-turbo
|
||||
[submodule "native/src/main/jni/image/giflib/giflib"]
|
||||
path = native/src/main/jni/image/giflib/giflib
|
||||
url = https://gitlab.com/NekoInverter/giflib.git
|
||||
url = https://github.com/Ehviewer-Overhauled/giflib
|
||||
[submodule "native/src/main/jni/image/libpng/libpng"]
|
||||
path = native/src/main/jni/image/libpng/libpng
|
||||
url = https://gitlab.com/NekoInverter/libpng.git
|
||||
url = https://github.com/Ehviewer-Overhauled/libpng
|
||||
[submodule "native/src/main/jni/libarchive"]
|
||||
path = native/src/main/jni/libarchive
|
||||
url = https://github.com/Ehviewer-Overhauled/libarchive
|
||||
[submodule "native/src/main/jni/liblzma"]
|
||||
path = native/src/main/jni/liblzma
|
||||
url = https://github.com/Ehviewer-Overhauled/xz
|
||||
@@ -1,202 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Preamble
|
||||
|
||||
1. Definitions.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
0. Definitions.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
1. Source Code.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
2. Basic Permissions.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
-1840
File diff suppressed because it is too large.
Load diff
@@ -1,22 +1,50 @@
|
||||
### [下载](https://install.appcenter.ms/users/nekoinverter/apps/ehviewer/distribution_groups/public)
|
||||
<h1 align="center">
|
||||
<img src="art/launcher_icon-web.png" width="200">
|
||||
<br>EhViewer<br>
|
||||
</h1>
|
||||
|
||||
[Download](https://install.appcenter.ms/users/nekoinverter/apps/ehviewer/distribution_groups/public)
|
||||
<p align="center">
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||
</a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
|
||||
</a>
|
||||
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
|
||||
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
|
||||
</a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
|
||||
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
|
||||
</a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
|
||||
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# EhViewer
|
||||
## Description
|
||||
这是一个 E-Hentai Android 平台的浏览器
|
||||
|
||||

|
||||
已针对性适配 Material Design 3 及其动态取色系统
|
||||
|
||||
动态配色仅供 Android 12 及以上平台使用!
|
||||
|
||||
这是一个 E-Hentai Android 平台的浏览器。
|
||||
|
||||
An E-Hentai Application for Android.
|
||||
|
||||
with Material Design 3 and Dynamic Color Support.
|
||||
|
||||
# Screenshot
|
||||
Dynamic Color only support Android S and above!
|
||||
|
||||
## Download
|
||||
|
||||
[Github 下载](https://github.com/Ehviewer-Overhauled/Ehviewer/releases)
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
# Thanks
|
||||
## Thanks
|
||||
|
||||
本项目受到了诸多开源项目的帮助
|
||||
|
||||
@@ -24,34 +52,31 @@ Here is the libraries
|
||||
|
||||
- [AOSP](http://source.android.com/)
|
||||
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
|
||||
- [material-design-icons](https://github.com/google/material-design-icons)
|
||||
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
- [apng](http://apng.sourceforge.net/)
|
||||
- [giflib](http://giflib.sourceforge.net)
|
||||
- [greenDAO](https://github.com/greenrobot/greenDAO)
|
||||
- [jsoup](https://github.com/jhy/jsoup)
|
||||
- [libjpeg-turbo](http://libjpeg-turbo.virtualgl.org/)
|
||||
- [libjpeg-turbo](https://libjpeg-turbo.org/)
|
||||
- [libpng](http://www.libpng.org/pub/png/libpng.html)
|
||||
- [okhttp](https://github.com/square/okhttp)
|
||||
- [roaster](https://github.com/forge/roaster)
|
||||
- [ShowcaseView](https://github.com/amlcurran/ShowcaseView)
|
||||
- [Slabo](https://github.com/TiroTypeworks/Slabo)
|
||||
- [TagSoup](http://home.ccil.org/~cowan/tagsoup/)
|
||||
- [TapTargetView](https://github.com/KeepSafe/TapTargetView)
|
||||
- [libarchive](http://www.libarchive.org/)
|
||||
- [XZ Utils](https://tukaani.org/xz/)
|
||||
|
||||
|
||||
# License
|
||||
## License
|
||||
|
||||
Copyright (C) 2014-2019 Hippo Seven
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022 Tarsin Norbin
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
You should have received a copy of the GNU General Public License along with EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
ic_launcher 图标为 Hippo Seven 所有,所有权利保留
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fasuka-mio%2FEhviewer?ref=badge_large)
|
||||
+82
-71
@@ -1,12 +1,12 @@
|
||||
import java.nio.file.Paths
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.0"
|
||||
|
||||
Properties localProperties = new Properties()
|
||||
if (project.rootProject.file('local.properties').exists()) {
|
||||
@@ -16,28 +16,27 @@ android {
|
||||
splits {
|
||||
abi {
|
||||
enable = true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
sign {
|
||||
storeFile file(localProperties.getProperty("storeFile"))
|
||||
storePassword localProperties.getProperty("storePassword")
|
||||
keyAlias localProperties.getProperty("keyAlias")
|
||||
keyPassword localProperties.getProperty("keyPassword")
|
||||
}
|
||||
sign {
|
||||
storeFile file(signStoreFile)
|
||||
storePassword signStorePassword
|
||||
keyAlias signkeyAlias
|
||||
keyPassword signkeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "io.github.nekoinverter.ehviewer"
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 32
|
||||
versionCode 100011
|
||||
versionName "1.7.26.4"
|
||||
targetSdkVersion 33
|
||||
versionCode 114531
|
||||
versionName "1.7.28.4"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -45,6 +44,10 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '11'
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
checkReleaseBuilds false
|
||||
@@ -67,17 +70,11 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
signingConfig signingConfigs.sign
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +83,7 @@ android {
|
||||
java.srcDirs += 'src/main/java-gen'
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
}
|
||||
|
||||
@@ -94,63 +92,76 @@ tasks.withType(JavaCompile) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.activity:activity:1.4.0'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'androidx.core:core:1.7.0'
|
||||
implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'androidx.fragment:fragment:1.4.1'
|
||||
def activity_version = "1.6.0-alpha05"
|
||||
implementation "androidx.activity:activity:$activity_version"
|
||||
implementation "androidx.activity:activity-ktx:$activity_version"
|
||||
|
||||
implementation "androidx.annotation:annotation:1.4.0"
|
||||
implementation "androidx.annotation:annotation-experimental:1.2.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.0-alpha05"
|
||||
|
||||
def arch_core_version = "2.1.0"
|
||||
implementation "androidx.arch.core:core-common:$arch_core_version"
|
||||
implementation "androidx.arch.core:core-runtime:$arch_core_version"
|
||||
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
|
||||
def collection_version = "1.3.0-alpha01"
|
||||
implementation "androidx.collection:collection:$collection_version"
|
||||
implementation "androidx.collection:collection-ktx:$collection_version"
|
||||
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
|
||||
def core_version = "1.9.0-alpha05"
|
||||
implementation "androidx.core:core:$core_version"
|
||||
implementation "androidx.core:core-ktx:$core_version"
|
||||
|
||||
implementation "androidx.customview:customview:1.2.0-alpha01"
|
||||
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
|
||||
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
|
||||
|
||||
def fragment_version = "1.5.0"
|
||||
implementation "androidx.fragment:fragment:$fragment_version"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
|
||||
def lifecycle_version = "2.6.0-alpha01"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
|
||||
|
||||
implementation "androidx.loader:loader:1.1.0"
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
|
||||
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation "androidx.transition:transition-ktx:1.4.1"
|
||||
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
|
||||
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
|
||||
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.webkit:webkit:1.5.0-beta01'
|
||||
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.6.0'
|
||||
implementation 'com.getkeepsafe.relinker:relinker:1.4.4'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha03'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
implementation 'com.github.seven332:beerbelly:0.1.4'
|
||||
implementation 'com.github.seven332:glgallery:0.1.2'
|
||||
implementation 'com.github.seven332:glview:0.1.0'
|
||||
implementation 'com.github.seven332:glview-image:0.1.0'
|
||||
implementation 'com.github.seven332:streampipe:0.1.0'
|
||||
implementation 'com.github.seven332:tuxiang:0.1.6'
|
||||
implementation 'com.github.seven332:unifile:9ec57bcd8f'
|
||||
implementation 'com.github.seven332:yorozuya-thread:0.1.1'
|
||||
implementation 'com.github.seven332:yorozuya-collect:0.1.4'
|
||||
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
|
||||
implementation 'com.microsoft.appcenter:appcenter-analytics:4.4.3'
|
||||
implementation 'com.microsoft.appcenter:appcenter-crashes:4.4.3'
|
||||
implementation 'com.microsoft.appcenter:appcenter-distribute:4.4.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.2'
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
implementation 'com.gitlab.NekoInverter:yorozuya:723dc867a9'
|
||||
implementation 'dev.rikka.rikkax.appcompat:appcompat:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.core:core:1.3.4'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.2.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.material:material:1.6.6'
|
||||
implementation 'dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1'
|
||||
implementation 'dev.rikka.rikkax.widget:borderview:1.1.0'
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
implementation 'org.greenrobot:greendao:3.3.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.2'
|
||||
implementation projects.native
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.2.1'
|
||||
testImplementation 'org.jooq:joor:0.9.6'
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'com.github.seven332:glgallery:25893283ca'
|
||||
force 'com.github.seven332:glview:ba6aee61d7'
|
||||
force 'com.github.seven332:glview-image:68d94b0fc2'
|
||||
|
||||
exclude group: 'com.github.seven332', module: 'okhttp'
|
||||
exclude group: 'com.github.seven332', module: 'image'
|
||||
exclude group: 'com.github.seven332', module: 'yorozuya'
|
||||
exclude group: 'androidx.appcompat', module: 'appcompat'
|
||||
}
|
||||
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
|
||||
exclude group: 'dev.rikka.rikkax.material', module: 'material'
|
||||
}
|
||||
Binary file not shown.
@@ -1,13 +0,0 @@
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Environment;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class CardBgTest extends TestCase {
|
||||
|
||||
private static final float[] TOP_ALPHA = {
|
||||
0f, // Dump
|
||||
0.12f,
|
||||
0.16f,
|
||||
0.19f,
|
||||
0.25f,
|
||||
0.30f,
|
||||
};
|
||||
|
||||
private static final float[] TOP_OFFSET = {
|
||||
0f, // Dump
|
||||
1f,
|
||||
3f,
|
||||
10f,
|
||||
14f,
|
||||
19f,
|
||||
};
|
||||
|
||||
private static final float[] TOP_BLUR = {
|
||||
0f, // Dump
|
||||
1.5f,
|
||||
3f,
|
||||
10f,
|
||||
14f,
|
||||
19f,
|
||||
};
|
||||
|
||||
private static final float[] BOTTOM_ALPHA = {
|
||||
0f, // Dump
|
||||
0.24f,
|
||||
0.23f,
|
||||
0.23f,
|
||||
0.22f,
|
||||
0.22f,
|
||||
};
|
||||
|
||||
private static final float[] BOTTOM_OFFSET = {
|
||||
0f, // Dump
|
||||
1f,
|
||||
3f,
|
||||
6f,
|
||||
10f,
|
||||
15f,
|
||||
};
|
||||
|
||||
private static final float[] BOTTOM_BLUR = {
|
||||
0f, // Dump
|
||||
1f,
|
||||
3f,
|
||||
3f,
|
||||
5f,
|
||||
6f,
|
||||
};
|
||||
|
||||
private static final int MAX_SIZE = 200;
|
||||
|
||||
|
||||
public void testGen() throws FileNotFoundException {
|
||||
genCardBg(Color.WHITE, 2, 2);
|
||||
}
|
||||
|
||||
private Path getPath(float radius, float base) {
|
||||
RectF rectF = new RectF();
|
||||
Path path = new Path();
|
||||
path.moveTo(base / 2, -base / 2 - radius);
|
||||
rectF.set(base / 2 - radius, -base / 2 - radius, base / 2 + radius, -base / 2 + radius);
|
||||
path.arcTo(rectF, -90, 90);
|
||||
path.lineTo(base / 2 + radius, base / 2);
|
||||
rectF.set(base / 2 - radius, base / 2 - radius, base / 2 + radius, base / 2 + radius);
|
||||
path.arcTo(rectF, 0, 90);
|
||||
path.lineTo(-base / 2, base / 2 + radius);
|
||||
rectF.set(-base / 2 - radius, base / 2 - radius, -base / 2 + radius, base / 2 + radius);
|
||||
path.arcTo(rectF, 90, 90);
|
||||
path.lineTo(-base / 2 - radius, -base / 2);
|
||||
rectF.set(-base / 2 - radius, -base / 2 - radius, -base / 2 + radius, -base / 2 + radius);
|
||||
path.arcTo(rectF, 180, 90);
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
private void genCardBg(int color, float radius, int elevation) throws FileNotFoundException {
|
||||
|
||||
float base = 4;
|
||||
float topAlpha = TOP_ALPHA[elevation];
|
||||
float topOffset = TOP_OFFSET[elevation];
|
||||
float topBlur = TOP_BLUR[elevation];
|
||||
float bottomAlpha = BOTTOM_ALPHA[elevation];
|
||||
float bottomOffset = BOTTOM_OFFSET[elevation];
|
||||
float bottomBlur = BOTTOM_BLUR[elevation];
|
||||
|
||||
|
||||
float ratio = 3;
|
||||
|
||||
|
||||
doGenCardBg(new FileOutputStream(new File(Environment.getExternalStorageDirectory(), "test.png")),
|
||||
color, radius * ratio, base * ratio,
|
||||
topAlpha, topOffset * ratio, topBlur * ratio,
|
||||
bottomAlpha, bottomOffset * ratio, bottomBlur * ratio);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private int getColor(float alpha) {
|
||||
return Color.argb((int) (alpha * 0xff), 0, 0, 0);
|
||||
}
|
||||
|
||||
private void doGenCardBg(OutputStream os, int color, float radius, float base,
|
||||
float topAlpha, float topOffset, float topBlur,
|
||||
float bottomAlpha, float bottomOffset, float bottomBlur) throws FileNotFoundException {
|
||||
|
||||
Path path = getPath(radius, base);
|
||||
path.offset(MAX_SIZE / 2, MAX_SIZE / 2);
|
||||
|
||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
paint.setColor(color);
|
||||
Bitmap bitmap = Bitmap.createBitmap(MAX_SIZE, MAX_SIZE, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
// Draw bottom
|
||||
paint.setShadowLayer(bottomBlur, 0, bottomOffset, getColor(bottomAlpha));
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Draw top
|
||||
paint.setShadowLayer(topBlur, 0, topOffset, getColor(topAlpha));
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
public class DumpTest extends TestCase {
|
||||
|
||||
|
||||
public void testDump() {
|
||||
String str = "M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55";
|
||||
System.out.println(str.replaceAll("\\d+(\\.\\d+)?", "12"));
|
||||
}
|
||||
|
||||
public void testDumpURL() {
|
||||
String str1 = "http://exhentai.org/?f_doujinshi=1&f_manga=0&f_artistcg=0&f_gamecg=1&f_western=1&f_non-h=1&f_imageset=0&f_cosplay=0&f_asianporn=1&f_misc=1&f_search=&f_apply=Apply+Filter";
|
||||
String str2 = "http://exhentai.org/3?f_search=hentai&f_apply=Apply+Filter";
|
||||
try {
|
||||
URL url = new URL(str2);
|
||||
|
||||
|
||||
System.out.println(url.getHost());
|
||||
System.out.println(url.getPath());
|
||||
System.out.println(url.getQuery());
|
||||
|
||||
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_start"
|
||||
android:shortcutDisabledMessage="@string/download_start_all"
|
||||
android:shortcutId="start_all"
|
||||
android:shortcutLongLabel="@string/download_start_all"
|
||||
android:shortcutShortLabel="@string/download_start_all">
|
||||
<intent
|
||||
android:action="start_all"
|
||||
android:targetClass="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
|
||||
android:targetPackage="io.github.nekoinverter.ehviewer.debug" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_shortcut_stop"
|
||||
android:shortcutDisabledMessage="@string/download_stop_all"
|
||||
android:shortcutId="stop_all"
|
||||
android:shortcutLongLabel="@string/download_stop_all"
|
||||
android:shortcutShortLabel="@string/download_stop_all">
|
||||
<intent
|
||||
android:action="stop_all"
|
||||
android:targetClass="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
|
||||
android:targetPackage="io.github.nekoinverter.ehviewer.debug" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
|
||||
</shortcuts>
|
||||
@@ -14,17 +14,23 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
|
||||
<!-- Required to maintain app compatibility. -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
@@ -32,29 +38,35 @@
|
||||
<application
|
||||
android:name="com.hippo.ehviewer.EhApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_scheme"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:appCategory="image"
|
||||
android:hasFragileUserData="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.MainActivity"
|
||||
android:banner="@mipmap/ic_leanback_banner"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
|
||||
android:exported="true">
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
@@ -89,9 +101,8 @@
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.GalleryActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.Gallery"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,45 +0,0 @@
|
||||
package androidx.recyclerview.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class BridgeRecyclerView extends RecyclerView {
|
||||
public BridgeRecyclerView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BridgeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BridgeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
|
||||
try {
|
||||
super.scrollStep(dx, dy, consumed);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
|
||||
return super.scrollByInternal(x, y, ev, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
try {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.app;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
public class CheckBoxDialogBuilder extends AlertDialog.Builder {
|
||||
|
||||
private final CheckBox mCheckBox;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public CheckBoxDialogBuilder(Context context, String message, String checkText, boolean checked) {
|
||||
super(context);
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null);
|
||||
setView(view);
|
||||
setMessage(message);
|
||||
mCheckBox = view.findViewById(R.id.checkbox);
|
||||
mCheckBox.setText(checkText);
|
||||
mCheckBox.setChecked(checked);
|
||||
view.setOnClickListener(v -> mCheckBox.toggle());
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mCheckBox.isChecked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.app
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.CheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class CheckBoxDialogBuilder constructor(
|
||||
context: Context,
|
||||
message: String?,
|
||||
checkText: String?,
|
||||
checked: Boolean
|
||||
) : MaterialAlertDialogBuilder(
|
||||
context
|
||||
) {
|
||||
private val mCheckBox: CheckBox
|
||||
val isChecked: Boolean
|
||||
get() = mCheckBox.isChecked
|
||||
|
||||
init {
|
||||
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)
|
||||
setView(view)
|
||||
setMessage(message)
|
||||
mCheckBox = view.findViewById(R.id.checkbox)
|
||||
mCheckBox.text = checkText
|
||||
mCheckBox.isChecked = checked
|
||||
view.setOnClickListener { mCheckBox.toggle() }
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.app;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
public class EditTextDialogBuilder extends AlertDialog.Builder implements EditText.OnEditorActionListener {
|
||||
|
||||
private final TextInputLayout mTextInputLayout;
|
||||
private final EditText mEditText;
|
||||
private AlertDialog mDialog;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public EditTextDialogBuilder(Context context, String text, String hint) {
|
||||
super(context);
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null);
|
||||
setView(view);
|
||||
mTextInputLayout = (TextInputLayout) view;
|
||||
mEditText = view.findViewById(R.id.edit_text);
|
||||
mEditText.setText(text);
|
||||
mEditText.setSelection(mEditText.getText().length());
|
||||
mEditText.setOnEditorActionListener(this);
|
||||
mTextInputLayout.setHint(hint);
|
||||
}
|
||||
|
||||
public EditText getEditText() {
|
||||
return mEditText;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return mEditText.getText().toString();
|
||||
}
|
||||
|
||||
public void setError(CharSequence error) {
|
||||
mTextInputLayout.setError(error);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AlertDialog create() {
|
||||
mDialog = super.create();
|
||||
return mDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (mDialog != null) {
|
||||
Button button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
if (button != null) {
|
||||
button.performClick();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class EditTextDialogBuilder constructor(
|
||||
context: Context,
|
||||
text: String?,
|
||||
hint: String?
|
||||
) : MaterialAlertDialogBuilder(
|
||||
context
|
||||
), OnEditorActionListener {
|
||||
private val mTextInputLayout: TextInputLayout
|
||||
val editText: EditText
|
||||
private var mDialog: AlertDialog? = null
|
||||
val text: String
|
||||
get() = editText.text.toString()
|
||||
|
||||
fun setError(error: CharSequence?) {
|
||||
mTextInputLayout.error = error
|
||||
}
|
||||
|
||||
override fun create(): AlertDialog {
|
||||
mDialog = super.create()
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
|
||||
return if (mDialog != null) {
|
||||
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
button?.performClick()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)
|
||||
setView(view)
|
||||
mTextInputLayout = view as TextInputLayout
|
||||
editText = view.findViewById(R.id.edit_text)
|
||||
editText.setText(text)
|
||||
editText.setSelection(editText.text.length)
|
||||
editText.setOnEditorActionListener(this)
|
||||
mTextInputLayout.hint = hint
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.app;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class ListCheckBoxDialogBuilder extends AlertDialog.Builder {
|
||||
|
||||
private final CheckBox mCheckBox;
|
||||
|
||||
private AlertDialog mDialog;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public ListCheckBoxDialogBuilder(Context context, CharSequence[] items,
|
||||
final OnItemClickListener listener, String checkText, boolean checked) {
|
||||
super(context);
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null);
|
||||
setView(view);
|
||||
ListView listView = (ListView) ViewUtils.$$(view, R.id.list_view);
|
||||
mCheckBox = (CheckBox) ViewUtils.$$(view, R.id.checkbox);
|
||||
listView.setAdapter(new ArrayAdapter<>(getContext(), R.layout.item_select_dialog, items));
|
||||
listView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
if (listener != null) {
|
||||
listener.onItemClick(ListCheckBoxDialogBuilder.this, mDialog, position);
|
||||
}
|
||||
mDialog.dismiss();
|
||||
});
|
||||
mCheckBox.setText(checkText);
|
||||
mCheckBox.setChecked(checked);
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mCheckBox.isChecked();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AlertDialog create() {
|
||||
mDialog = super.create();
|
||||
return mDialog;
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(ListCheckBoxDialogBuilder builder, AlertDialog dialog, int position);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.app
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class ListCheckBoxDialogBuilder constructor(
|
||||
context: Context, items: Array<CharSequence>,
|
||||
listener: OnItemClickListener?, checkText: String?, checked: Boolean
|
||||
) : MaterialAlertDialogBuilder(
|
||||
context
|
||||
) {
|
||||
private val mCheckBox: CheckBox
|
||||
private var mDialog: AlertDialog? = null
|
||||
val isChecked: Boolean
|
||||
get() = mCheckBox.isChecked
|
||||
|
||||
override fun create(): AlertDialog {
|
||||
mDialog = super.create()
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
|
||||
}
|
||||
|
||||
init {
|
||||
val view =
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
|
||||
setView(view)
|
||||
val listView = ViewUtils.`$$`(view, R.id.list_view) as ListView
|
||||
mCheckBox = ViewUtils.`$$`(view, R.id.checkbox) as CheckBox
|
||||
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
|
||||
listView.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
mDialog?.dismiss()
|
||||
}
|
||||
mCheckBox.text = checkText
|
||||
mCheckBox.isChecked = checked
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public abstract class BeerBelly<V> {
|
||||
|
||||
private static final String TAG = BeerBelly.class.getSimpleName();
|
||||
private final boolean mHasMemoryCache;
|
||||
private final boolean mHasDiskCache;
|
||||
@Nullable
|
||||
private MemoryCache<V> mMemoryCache;
|
||||
@Nullable
|
||||
private DiskCache<V> mDiskCache;
|
||||
|
||||
public BeerBelly(BeerBellyParams params) {
|
||||
params.isValid();
|
||||
mHasMemoryCache = params.hasMemoryCache;
|
||||
mHasDiskCache = params.hasDiskCache;
|
||||
|
||||
if (mHasMemoryCache) {
|
||||
initMemoryCache(params.memoryCacheMaxSize);
|
||||
}
|
||||
|
||||
if (mHasDiskCache) {
|
||||
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void initMemoryCache(int maxSize) {
|
||||
mMemoryCache = new MemoryCache<>(maxSize, this);
|
||||
}
|
||||
|
||||
private void initDiskCache(File cacheDir, int maxSize) {
|
||||
// Set up disk cache
|
||||
try {
|
||||
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Can't create disk cache", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int sizeOf(String key, V value);
|
||||
|
||||
protected abstract void memoryEntryAdded(V value);
|
||||
|
||||
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
|
||||
|
||||
protected abstract V read(@NonNull InputStreamPipe isPipe);
|
||||
|
||||
protected abstract boolean write(OutputStream os, V value);
|
||||
|
||||
@Nullable
|
||||
public LruCache<String, V> getMemoryCache() {
|
||||
return mMemoryCache;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SimpleDiskCache getDiskCache() {
|
||||
if (mDiskCache != null) {
|
||||
return mDiskCache.mDiskCache;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have memory cache
|
||||
*
|
||||
* @return true if have memory cache
|
||||
*/
|
||||
public boolean hasMemoryCache() {
|
||||
return mHasMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have disk cache
|
||||
*
|
||||
* @return true if have disk cache
|
||||
*/
|
||||
public boolean hasDiskCache() {
|
||||
return mHasDiskCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache usage size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memorySize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.size();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache max size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memoryMaxSize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.maxSize();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache usage size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache max size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskMaxSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.getMaxSize();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache
|
||||
*/
|
||||
public V getFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache or get error
|
||||
*/
|
||||
public V getFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache and disk cache. If miss in memory cache and
|
||||
* get in disk cache, it will put value from disk cache to memory cache.
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get
|
||||
*/
|
||||
public V get(@NonNull String key) {
|
||||
V value = getFromMemory(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in memory cache
|
||||
return value;
|
||||
}
|
||||
|
||||
value = getFromDisk(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in disk cache
|
||||
putToMemory(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no memory cache
|
||||
*/
|
||||
public boolean putToMemory(@NonNull String key, @NonNull V value) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
memoryEntryAdded(value);
|
||||
mMemoryCache.put(key, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putToDisk(@NonNull String key, @NonNull V value) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.put(key, value);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache and disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
public void put(@NonNull String key, @NonNull V value) {
|
||||
putToMemory(key, value);
|
||||
putToDisk(key, value);
|
||||
}
|
||||
|
||||
public void removeFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(@NonNull String key) {
|
||||
removeFromMemory(key);
|
||||
removeFromDisk(key);
|
||||
}
|
||||
|
||||
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.pull(key, os);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key
|
||||
* @param is the input stream to store
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.putRaw(key, is);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache
|
||||
*/
|
||||
public void clearMemory() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear disk cache
|
||||
*/
|
||||
public void clearDisk() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache and lets the system know
|
||||
* now would be a good time to garbage collect
|
||||
*/
|
||||
public void clear() {
|
||||
clearMemory();
|
||||
clearDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder class that contains cache parameters.
|
||||
*/
|
||||
public static class BeerBellyParams {
|
||||
|
||||
/**
|
||||
* is memory cache available
|
||||
*/
|
||||
public boolean hasMemoryCache = false;
|
||||
/**
|
||||
* the maximum number of bytes the memory cache should use to store
|
||||
*/
|
||||
public int memoryCacheMaxSize = 0;
|
||||
/**
|
||||
* is disk cache available
|
||||
*/
|
||||
public boolean hasDiskCache = false;
|
||||
/**
|
||||
* the dir to store disk cache
|
||||
*/
|
||||
public File diskCacheDir = null;
|
||||
/**
|
||||
* the maximum number of bytes the disk cache should use to store
|
||||
*/
|
||||
public int diskCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Check BeerBellyParams is valid
|
||||
*/
|
||||
public void isValid() throws IllegalStateException {
|
||||
if (!hasMemoryCache && !hasDiskCache) {
|
||||
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
|
||||
}
|
||||
|
||||
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
|
||||
}
|
||||
|
||||
if (hasDiskCache) {
|
||||
if (diskCacheDir == null) {
|
||||
throw new IllegalStateException("Disk cache dir can't be null.");
|
||||
}
|
||||
if (diskCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiskCache<E> {
|
||||
|
||||
private static final int IO_BUFFER_SIZE = 8 * 1024;
|
||||
|
||||
private final SimpleDiskCache mDiskCache;
|
||||
private final BeerBelly<E> mParent;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mMaxSize;
|
||||
|
||||
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
|
||||
mDiskCache = new SimpleDiskCache(cacheDir, size);
|
||||
mParent = parent;
|
||||
|
||||
mCacheDir = cacheDir;
|
||||
mMaxSize = size;
|
||||
}
|
||||
|
||||
public File getCacheDir() {
|
||||
return mCacheDir;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return mMaxSize;
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return mDiskCache.size();
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
|
||||
public E get(String key) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return null;
|
||||
} else {
|
||||
return mParent.read(isPipe);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean put(String key, E value) {
|
||||
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
|
||||
BufferedOutputStream buffOut = null;
|
||||
try {
|
||||
osPipe.obtain();
|
||||
OutputStream os = osPipe.open();
|
||||
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
|
||||
return mParent.write(os, value);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
Util.closeQuietly(buffOut);
|
||||
osPipe.close();
|
||||
osPipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean putRaw(String key, InputStream is) {
|
||||
return mDiskCache.put(key, is);
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
|
||||
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return false;
|
||||
} else {
|
||||
try {
|
||||
isPipe.obtain();
|
||||
InputStream is = isPipe.open();
|
||||
Util.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
isPipe.close();
|
||||
isPipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MemoryCache<E> extends LruCache<String, E> {
|
||||
|
||||
public BeerBelly<E> mParent;
|
||||
|
||||
public MemoryCache(int maxSize, BeerBelly<E> parent) {
|
||||
super(maxSize);
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(String key, E value) {
|
||||
return mParent.sizeOf(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
|
||||
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Each key must match
|
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
|
||||
* accessible as streams or files. Each value must be between {@code 0} and
|
||||
* {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <p>The cache stores its data in a directory on the filesystem. This
|
||||
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
|
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
/*
|
||||
* This cache uses a journal file named "journal". A typical journal file
|
||||
* looks like this:
|
||||
* libcore.io.DiskLruCache
|
||||
* 1
|
||||
* 100
|
||||
* 2
|
||||
*
|
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
*
|
||||
* The first five lines of the journal form its header. They are the
|
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
* the application's version, the value count, and a blank line.
|
||||
*
|
||||
* Each of the subsequent lines in the file is a record of the state of a
|
||||
* cache entry. Each line contains space-separated values: a state, a key,
|
||||
* and optional state-specific values.
|
||||
* o DIRTY lines track that an entry is actively being created or updated.
|
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
* temporary files may need to be deleted.
|
||||
* o CLEAN lines track a cache entry that has been successfully published
|
||||
* and may be read. A publish line is followed by the lengths of each of
|
||||
* its values.
|
||||
* o READ lines track accesses for LRU.
|
||||
* o REMOVE lines track entries that have been deleted.
|
||||
*
|
||||
* The journal file is appended to as cache operations occur. The journal may
|
||||
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
* it exists when the cache is opened.
|
||||
*/
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// Eat all writes silently. Nom nom.
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This cache uses a single background thread to evict entries.
|
||||
*/
|
||||
final ThreadPoolExecutor executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final File journalFileBackup;
|
||||
private final int appVersion;
|
||||
private final int valueCount;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<>(0, 0.75f, true);
|
||||
private long maxSize;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private int redundantOpCount;
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0;
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
} private final Callable<Void> cleanupCallable = new Callable<>() {
|
||||
public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // Closed.
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
if (backupFile.exists()) {
|
||||
File journalFile = new File(directory, JOURNAL_FILE);
|
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) {
|
||||
backupFile.delete();
|
||||
} else {
|
||||
renameTo(backupFile, journalFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
System.out
|
||||
.println("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||
if (deleteDestination) {
|
||||
deleteIfExists(to);
|
||||
}
|
||||
if (!from.renameTo(to)) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic)
|
||||
|| !VERSION_1.equals(version)
|
||||
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||
|| !"".equals(blank)) {
|
||||
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||
+ valueCountString + ", " + blank + "]");
|
||||
}
|
||||
|
||||
int lineCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
lineCount++;
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
redundantOpCount = lineCount - lruEntries.size();
|
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) {
|
||||
rebuildJournal();
|
||||
} else {
|
||||
journalWriter = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
int keyBegin = firstSpace + 1;
|
||||
int secondSpace = line.indexOf(' ', keyBegin);
|
||||
final String key;
|
||||
if (secondSpace == -1) {
|
||||
key = line.substring(keyBegin);
|
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = line.substring(keyBegin, secondSpace);
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(parts);
|
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||
// This work was already done by calling lruEntries.get().
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
try (Writer writer = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (journalFile.exists()) {
|
||||
renameTo(journalFile, journalFileBackup, true);
|
||||
}
|
||||
renameTo(journalFileTmp, journalFile, false);
|
||||
journalFileBackup.delete();
|
||||
|
||||
journalWriter = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if it contains the key
|
||||
*/
|
||||
public synchronized boolean contain(String key) {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
return entry != null && entry.readable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (ins[i] != null) {
|
||||
Util.closeQuietly(ins[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // Snapshot is stale.
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // Another edit is in progress.
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where this cache stores its data.
|
||||
*/
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public synchronized long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum number of bytes the cache can store and queues a job
|
||||
* to trim the existing store, if necessary.
|
||||
*/
|
||||
public synchronized void setMaxSize(long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
journalWriter.flush();
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cache has been closed.
|
||||
*/
|
||||
public synchronized boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force buffered operations to the filesystem.
|
||||
*/
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this cache. Stored values will remain on the filesystem.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // Already closed.
|
||||
}
|
||||
for (Entry entry : new ArrayList<>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache and deletes all of its stored values. This will delete
|
||||
* all files in the cache directory including files that weren't created by
|
||||
* the cache.
|
||||
*/
|
||||
public void delete() throws IOException {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("keys must match regex "
|
||||
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the values for an entry.
|
||||
*/
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
private final long[] lengths;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
this.lengths = lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbuffered stream with the value for {@code index}.
|
||||
*/
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of the value for {@code index}.
|
||||
*/
|
||||
public long getLength(int index) {
|
||||
return lengths[index];
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
private boolean committed;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
if (index < 0 || index >= valueCount) {
|
||||
throw new IllegalArgumentException("Expected index " + index + " to "
|
||||
+ "be greater than 0 and less than the maximum value count "
|
||||
+ "of " + valueCount);
|
||||
}
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
File dirtyFile = entry.getDirtyFile(index);
|
||||
FileOutputStream outputStream;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs();
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e2) {
|
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM;
|
||||
}
|
||||
}
|
||||
return new FaultHidingOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at {@code index} to {@code value}.
|
||||
*/
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
public void abortUnlessCommitted() {
|
||||
if (!committed) {
|
||||
try {
|
||||
abort();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Lengths of this entry's files.
|
||||
*/
|
||||
private final long[] lengths;
|
||||
|
||||
/**
|
||||
* True if this entry has ever been published.
|
||||
*/
|
||||
private boolean readable;
|
||||
|
||||
/**
|
||||
* The ongoing edit or null if this entry is not being edited.
|
||||
*/
|
||||
private Editor currentEditor;
|
||||
|
||||
/**
|
||||
* The sequence number of the most recently committed edit to this entry.
|
||||
*/
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class SimpleDiskCache {
|
||||
|
||||
private static final String TAG = SimpleDiskCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Disk cache is free
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Disk cache is flushing or clearing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_IN_USE = 1;
|
||||
|
||||
/**
|
||||
* Disk cache is reading or writing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_BUSY = 2;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mSize;
|
||||
@NonNull
|
||||
private final Map<String, CounterLock> mLockMap = new HashMap<>();
|
||||
@NonNull
|
||||
private final LockPool mLockPool = new LockPool();
|
||||
private int mDiskCacheState;
|
||||
@Nullable
|
||||
private DiskLruCache mDiskLruCache;
|
||||
|
||||
public SimpleDiskCache(File cacheDir, int size) {
|
||||
mCacheDir = cacheDir;
|
||||
mSize = size;
|
||||
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
private static String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
private static String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
private synchronized void init() throws IOException {
|
||||
if (!isValid()) {
|
||||
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long size() {
|
||||
if (null != mDiskLruCache) {
|
||||
return mDiskLruCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
if (null != mDiskLruCache) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache flush", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized boolean clear() {
|
||||
boolean result = true;
|
||||
if (null == mDiskLruCache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache clearCache", e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache init", e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized CounterLock obtainLock(String key) {
|
||||
// Wait for clear over
|
||||
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
|
||||
|
||||
// Get lock from key
|
||||
CounterLock lock;
|
||||
lock = mLockMap.get(key);
|
||||
if (lock == null) {
|
||||
lock = mLockPool.get();
|
||||
mLockMap.put(key, lock);
|
||||
}
|
||||
lock.obtain();
|
||||
|
||||
return lock;
|
||||
}
|
||||
|
||||
private synchronized void releaseLock(String key, CounterLock lock) {
|
||||
lock.release();
|
||||
if (lock.isFree()) {
|
||||
mLockMap.remove(key);
|
||||
mLockPool.push(lock);
|
||||
}
|
||||
|
||||
if (mLockMap.isEmpty()) {
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.readLock().lock();
|
||||
|
||||
boolean result = mDiskLruCache.contain(diskKey);
|
||||
|
||||
lock.readLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result;
|
||||
try {
|
||||
result = mDiskLruCache.remove(diskKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean put(@NonNull String key, @NonNull InputStream is) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result = false;
|
||||
if (isValid()) {
|
||||
result = putToDisk(diskKey, is);
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean putToDisk(String key, InputStream is) {
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream os = null;
|
||||
boolean completeEdit = false;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
editor = mDiskLruCache.edit(key);
|
||||
if (editor == null) {
|
||||
// The editor is in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
os = editor.newOutputStream(0);
|
||||
if (os != null) {
|
||||
Util.copy(is, os);
|
||||
completeEdit = true;
|
||||
editor.commit();
|
||||
return true;
|
||||
} else {
|
||||
// Can't get OutputStream
|
||||
completeEdit = true;
|
||||
editor.abort();
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(os);
|
||||
try {
|
||||
if (!completeEdit && editor != null) {
|
||||
editor.abort();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key of the target
|
||||
* @return the InputStreamPipe, <code>null</code> for missing
|
||||
*/
|
||||
@Nullable
|
||||
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
|
||||
if (contain(key)) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheInputStreamPipe(diskKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheOutputStreamPipe(diskKey);
|
||||
}
|
||||
|
||||
private class CacheInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Snapshot mCurrentSnapshot;
|
||||
|
||||
private CacheInputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.readLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
InputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
snapshot = mDiskLruCache.get(mKey);
|
||||
if (snapshot == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
mCurrentSnapshot = snapshot;
|
||||
return snapshot.getInputStream(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Util.closeQuietly(mCurrentSnapshot);
|
||||
mCurrentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Editor mCurrentEditor;
|
||||
|
||||
private CacheOutputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.writeLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
OutputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
|
||||
if (editor == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
|
||||
OutputStream os;
|
||||
try {
|
||||
os = editor.newOutputStream(0);
|
||||
mCurrentEditor = editor;
|
||||
return os;
|
||||
} catch (IOException e) {
|
||||
// Get trouble
|
||||
try {
|
||||
editor.abort();
|
||||
} catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCurrentEditor != null) {
|
||||
try {
|
||||
mCurrentEditor.commit();
|
||||
} catch (IOException e) {
|
||||
// Empty
|
||||
}
|
||||
mCurrentEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CounterLock extends ReentrantReadWriteLock {
|
||||
|
||||
private int mCount;
|
||||
|
||||
public boolean isFree() {
|
||||
return mCount == 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (--mCount < 0) {
|
||||
throw new IllegalStateException("Release time is more than occupy time");
|
||||
}
|
||||
}
|
||||
|
||||
public void obtain() {
|
||||
++mCount;
|
||||
}
|
||||
}
|
||||
|
||||
private class LockPool {
|
||||
|
||||
private static final int MAX_SIZE = 10;
|
||||
|
||||
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
|
||||
private final int mMaxSize = MAX_SIZE;
|
||||
private int mSize;
|
||||
|
||||
public void push(CounterLock lock) {
|
||||
if (lock != null && mSize < mMaxSize) {
|
||||
mArray[mSize++] = lock;
|
||||
}
|
||||
}
|
||||
|
||||
public CounterLock get() {
|
||||
if (mSize > 0) {
|
||||
CounterLock lock = mArray[--mSize];
|
||||
mArray[mSize] = null;
|
||||
return lock;
|
||||
} else {
|
||||
return new CounterLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
|
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
|
||||
* line at end of input is invalid and will be ignored, the caller may use {@code
|
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||
* end-of-input reporting and a more restrictive definition of a line.
|
||||
*
|
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
/*
|
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
*/
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(Util.US_ASCII))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset.name());
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
try {
|
||||
return new String(buf, 0, length, charset.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e); // Since we control the charset this will never happen.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasUnterminatedLine() {
|
||||
return end == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.beerbelly;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Junk drawer of utility methods.
|
||||
*/
|
||||
final class Util {
|
||||
static final Charset US_ASCII = StandardCharsets.US_ASCII;
|
||||
static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
static String readFully(Reader reader) throws IOException {
|
||||
try (reader) {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||
*/
|
||||
static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IOException("not a readable directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy bytes from an <code>InputStream</code> to an
|
||||
* <code>OutputStream</code>.
|
||||
*
|
||||
* @param input the InputStream
|
||||
* @param output the OutputStream
|
||||
* @return the number of bytes copied
|
||||
*/
|
||||
public static long copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -39,26 +39,17 @@ import okhttp3.OkHttpClient;
|
||||
@SuppressWarnings("rawtypes")
|
||||
public final class Conaco<V> {
|
||||
|
||||
private static final String TAG = Conaco.class.getSimpleName();
|
||||
|
||||
@IntDef({SOURCE_MEMORY, SOURCE_DISK, SOURCE_NETWORK})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Source {
|
||||
}
|
||||
|
||||
public static final int SOURCE_MEMORY = 0;
|
||||
public static final int SOURCE_DISK = 1;
|
||||
public static final int SOURCE_NETWORK = 2;
|
||||
|
||||
private static final String TAG = Conaco.class.getSimpleName();
|
||||
private final ValueHelper<V> mHelper;
|
||||
private final ValueCache<V> mCache;
|
||||
private final OkHttpClient mOkHttpClient;
|
||||
private final Register<V> mRegister;
|
||||
|
||||
private final SerialThreadExecutor mDiskExecutor;
|
||||
private final ThreadPoolExecutor mNetworkExecutor;
|
||||
private final AtomicInteger mIdGenerator;
|
||||
|
||||
private final boolean mDebug;
|
||||
|
||||
private Conaco(Builder<V> builder) {
|
||||
@@ -197,6 +188,11 @@ public final class Conaco<V> {
|
||||
onUnregisterConacoTask(task);
|
||||
}
|
||||
|
||||
@IntDef({SOURCE_MEMORY, SOURCE_DISK, SOURCE_NETWORK})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Source {
|
||||
}
|
||||
|
||||
public static class Builder<T> extends BeerBelly.BeerBellyParams {
|
||||
/**
|
||||
* The client to get image from internet
|
||||
|
||||
@@ -85,6 +85,42 @@ public final class ConacoTask<V> {
|
||||
mConaco = builder.mConaco;
|
||||
}
|
||||
|
||||
private static void putFromDiskCacheToDataContainer(String key, ValueCache cache, DataContainer container) {
|
||||
SimpleDiskCache diskCache = cache.getDiskCache();
|
||||
if (diskCache != null) {
|
||||
InputStreamPipe pipe = diskCache.getInputStreamPipe(key);
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
container.save(pipe.open(), -1L, null, null);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Can't save value from disk cache to data container");
|
||||
e.printStackTrace();
|
||||
container.remove();
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void putFromDataContainerToDiskCache(String key, ValueCache cache, DataContainer container) {
|
||||
InputStreamPipe pipe = container.get();
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
cache.putRawToDisk(key, pipe.open());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Can't save value from data container to disk cache", e);
|
||||
cache.removeFromDisk(key);
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getId() {
|
||||
return mId;
|
||||
}
|
||||
@@ -176,39 +212,133 @@ public final class ConacoTask<V> {
|
||||
return mStop || asyncTask.isCancelled() || unikery == null || unikery.getTaskId() != mId;
|
||||
}
|
||||
|
||||
private static void putFromDiskCacheToDataContainer(String key, ValueCache cache, DataContainer container) {
|
||||
SimpleDiskCache diskCache = cache.getDiskCache();
|
||||
if (diskCache != null) {
|
||||
InputStreamPipe pipe = diskCache.getInputStreamPipe(key);
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
container.save(pipe.open(), -1L, null, null);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Can't save value from disk cache to data container");
|
||||
e.printStackTrace();
|
||||
container.remove();
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
|
||||
public static class Builder<T> {
|
||||
|
||||
private int mId;
|
||||
private Unikery<T> mUnikery;
|
||||
private String mKey;
|
||||
private String mUrl;
|
||||
private DataContainer mDataContainer;
|
||||
private boolean mUseMemoryCache = true;
|
||||
private boolean mUseDiskCache = true;
|
||||
private boolean mUseNetwork = true;
|
||||
private ValueHelper<T> mHelper;
|
||||
private ValueCache<T> mCache;
|
||||
private OkHttpClient mOkHttpClient;
|
||||
private Executor mDiskExecutor;
|
||||
private Executor mNetworkExecutor;
|
||||
private Conaco<T> mConaco;
|
||||
|
||||
public Builder<T> setId(int id) {
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unikery<T> getUnikery() {
|
||||
return mUnikery;
|
||||
}
|
||||
|
||||
public Builder<T> setUnikery(Unikery<T> unikery) {
|
||||
mUnikery = unikery;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public Builder<T> setKey(String key) {
|
||||
mKey = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
public Builder<T> setUrl(String url) {
|
||||
mUrl = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> setDataContainer(DataContainer dataContainer) {
|
||||
mDataContainer = dataContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseMemoryCache() {
|
||||
return mUseMemoryCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseMemoryCache(boolean useMemoryCache) {
|
||||
mUseMemoryCache = useMemoryCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseDiskCache() {
|
||||
return mUseDiskCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseDiskCache(boolean useDiskCache) {
|
||||
mUseDiskCache = useDiskCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseNetwork() {
|
||||
return mUseNetwork;
|
||||
}
|
||||
|
||||
public Builder<T> setUseNetwork(boolean useNetwork) {
|
||||
mUseNetwork = useNetwork;
|
||||
return this;
|
||||
}
|
||||
|
||||
ValueHelper<T> getHelper() {
|
||||
return mHelper;
|
||||
}
|
||||
|
||||
Builder<T> setHelper(ValueHelper<T> helper) {
|
||||
mHelper = helper;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setCache(ValueCache<T> cache) {
|
||||
mCache = cache;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setOkHttpClient(OkHttpClient okHttpClient) {
|
||||
mOkHttpClient = okHttpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setDiskExecutor(Executor diskExecutor) {
|
||||
mDiskExecutor = diskExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setNetworkExecutor(Executor networkExecutor) {
|
||||
mNetworkExecutor = networkExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setConaco(Conaco<T> conaco) {
|
||||
mConaco = conaco;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void isValid() {
|
||||
if (mUnikery == null) {
|
||||
throw new IllegalStateException("Must set unikery");
|
||||
}
|
||||
if (mKey == null && mUrl == null && mDataContainer == null) {
|
||||
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void putFromDataContainerToDiskCache(String key, ValueCache cache, DataContainer container) {
|
||||
InputStreamPipe pipe = container.get();
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.obtain();
|
||||
cache.putRawToDisk(key, pipe.open());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Can't save value from data container to disk cache", e);
|
||||
cache.removeFromDisk(key);
|
||||
} finally {
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
public ConacoTask<T> build() {
|
||||
return new ConacoTask<>(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,134 +585,4 @@ public final class ConacoTask<V> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
|
||||
public static class Builder<T> {
|
||||
|
||||
private int mId;
|
||||
private Unikery<T> mUnikery;
|
||||
private String mKey;
|
||||
private String mUrl;
|
||||
private DataContainer mDataContainer;
|
||||
private boolean mUseMemoryCache = true;
|
||||
private boolean mUseDiskCache = true;
|
||||
private boolean mUseNetwork = true;
|
||||
private ValueHelper<T> mHelper;
|
||||
private ValueCache<T> mCache;
|
||||
private OkHttpClient mOkHttpClient;
|
||||
private Executor mDiskExecutor;
|
||||
private Executor mNetworkExecutor;
|
||||
private Conaco<T> mConaco;
|
||||
|
||||
public Builder<T> setId(int id) {
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> setUnikery(Unikery<T> unikery) {
|
||||
mUnikery = unikery;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unikery<T> getUnikery() {
|
||||
return mUnikery;
|
||||
}
|
||||
|
||||
public Builder<T> setKey(String key) {
|
||||
mKey = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public Builder<T> setUrl(String url) {
|
||||
mUrl = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
public Builder<T> setDataContainer(DataContainer dataContainer) {
|
||||
mDataContainer = dataContainer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> setUseMemoryCache(boolean useMemoryCache) {
|
||||
mUseMemoryCache = useMemoryCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseMemoryCache() {
|
||||
return mUseMemoryCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseDiskCache(boolean useDiskCache) {
|
||||
mUseDiskCache = useDiskCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseDiskCache() {
|
||||
return mUseDiskCache;
|
||||
}
|
||||
|
||||
public Builder<T> setUseNetwork(boolean useNetwork) {
|
||||
mUseNetwork = useNetwork;
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean isUseNetwork() {
|
||||
return mUseNetwork;
|
||||
}
|
||||
|
||||
Builder<T> setHelper(ValueHelper<T> helper) {
|
||||
mHelper = helper;
|
||||
return this;
|
||||
}
|
||||
|
||||
ValueHelper<T> getHelper() {
|
||||
return mHelper;
|
||||
}
|
||||
|
||||
Builder<T> setCache(ValueCache<T> cache) {
|
||||
mCache = cache;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setOkHttpClient(OkHttpClient okHttpClient) {
|
||||
mOkHttpClient = okHttpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setDiskExecutor(Executor diskExecutor) {
|
||||
mDiskExecutor = diskExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setNetworkExecutor(Executor networkExecutor) {
|
||||
mNetworkExecutor = networkExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder<T> setConaco(Conaco<T> conaco) {
|
||||
mConaco = conaco;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void isValid() {
|
||||
if (mUnikery == null) {
|
||||
throw new IllegalStateException("Must set unikery");
|
||||
}
|
||||
if (mKey == null && mUrl == null && mDataContainer == null) {
|
||||
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
|
||||
}
|
||||
}
|
||||
|
||||
public ConacoTask<T> build() {
|
||||
return new ConacoTask<>(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,6 @@ public interface Unikery<V> {
|
||||
|
||||
int INVALID_ID = -1;
|
||||
|
||||
/**
|
||||
* Store the conaco task id
|
||||
*
|
||||
* @param id the conaco task id
|
||||
*/
|
||||
void setTaskId(int id);
|
||||
|
||||
/**
|
||||
* Get the conaco task id
|
||||
*
|
||||
@@ -36,6 +29,13 @@ public interface Unikery<V> {
|
||||
*/
|
||||
int getTaskId();
|
||||
|
||||
/**
|
||||
* Store the conaco task id
|
||||
*
|
||||
* @param id the conaco task id
|
||||
*/
|
||||
void setTaskId(int id);
|
||||
|
||||
/**
|
||||
* On miss in the source
|
||||
*/
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.database;
|
||||
|
||||
/*
|
||||
* Created by Hippo on 2017/9/3.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MSQLiteBuilder {
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
|
||||
private static final Map<Class, String> JAVA_TYPE_TO_SQLITE_TYPE = new HashMap<>();
|
||||
|
||||
static {
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(boolean.class, "INTEGER NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(byte.class, "INTEGER NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(short.class, "INTEGER NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(int.class, "INTEGER NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(long.class, "INTEGER NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(float.class, "REAL NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(double.class, "REAL NOT NULL DEFAULT 0");
|
||||
JAVA_TYPE_TO_SQLITE_TYPE.put(String.class, "TEXT");
|
||||
}
|
||||
|
||||
private int version = 0;
|
||||
private List<String> statements;
|
||||
private final SparseArray<List<String>> statementsMap = new SparseArray<>();
|
||||
|
||||
private static String javaTypeToSQLiteType(Class clazz) {
|
||||
String type = JAVA_TYPE_TO_SQLITE_TYPE.get(clazz);
|
||||
if (type == null) {
|
||||
throw new IllegalStateException("Unknown type: " + clazz);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bump database version.
|
||||
*/
|
||||
public MSQLiteBuilder version(int version) {
|
||||
if (version <= this.version) {
|
||||
throw new IllegalStateException("New version must be bigger than current version. "
|
||||
+ "current version: " + this.version + ", new version: " + version + ".");
|
||||
}
|
||||
this.version = version;
|
||||
this.statements = new ArrayList<>();
|
||||
this.statementsMap.put(version, this.statements);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table with int {@link #COLUMN_ID} primary key.
|
||||
*/
|
||||
public MSQLiteBuilder createTable(String table) {
|
||||
return createTable(table, COLUMN_ID, int.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table.
|
||||
*/
|
||||
public MSQLiteBuilder createTable(String table, String column, Class clazz) {
|
||||
return statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);");
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table.
|
||||
*/
|
||||
public MSQLiteBuilder dropTable(String table) {
|
||||
return statement("DROP TABLE " + table + ";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a column to the table.
|
||||
*/
|
||||
public MSQLiteBuilder insertColumn(String table, String column, Class clazz) {
|
||||
return statement("ALTER TABLE " + table + " ADD COLUMN " + column + " " + javaTypeToSQLiteType(clazz) + ";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a statement.
|
||||
*/
|
||||
public MSQLiteBuilder statement(String statement) {
|
||||
if (version == 0 || statements == null) {
|
||||
throw new IllegalStateException("Call version() first!");
|
||||
}
|
||||
statements.add(statement);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SQLiteOpenHelper from it.
|
||||
*/
|
||||
public SQLiteOpenHelper build(Context context, String name, int version) {
|
||||
return new MSQLiteOpenHelper(context, name, version, this);
|
||||
}
|
||||
|
||||
List<String> getStatements(int oldVersion, int newVersion) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (int i = oldVersion + 1; i <= newVersion; ++i) {
|
||||
List<String> list = statementsMap.get(i);
|
||||
if (list != null) {
|
||||
result.addAll(list);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2017 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.util.SparseArray
|
||||
|
||||
class MSQLiteBuilder {
|
||||
companion object {
|
||||
const val COLUMN_ID = "_id"
|
||||
private val JAVA_TYPE_TO_SQLITE_TYPE: MutableMap<Class<*>?, String> = HashMap()
|
||||
private fun javaTypeToSQLiteType(clazz: Class<*>?): String {
|
||||
return JAVA_TYPE_TO_SQLITE_TYPE[clazz]
|
||||
?: throw IllegalStateException("Unknown type: $clazz")
|
||||
}
|
||||
|
||||
init {
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Boolean::class.javaPrimitiveType] =
|
||||
"INTEGER NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Byte::class.javaPrimitiveType] =
|
||||
"INTEGER NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Short::class.javaPrimitiveType] =
|
||||
"INTEGER NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Int::class.javaPrimitiveType] = "INTEGER NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Long::class.javaPrimitiveType] =
|
||||
"INTEGER NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Float::class.javaPrimitiveType] =
|
||||
"REAL NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[Double::class.javaPrimitiveType] = "REAL NOT NULL DEFAULT 0"
|
||||
JAVA_TYPE_TO_SQLITE_TYPE[String::class.java] = "TEXT"
|
||||
}
|
||||
}
|
||||
|
||||
private val statementsMap = SparseArray<List<String>?>()
|
||||
private var version = 0
|
||||
private var statements: MutableList<String>? = null
|
||||
|
||||
/**
|
||||
* Bump database version.
|
||||
*/
|
||||
fun version(version: Int): MSQLiteBuilder {
|
||||
check(version > this.version) {
|
||||
("New version must be bigger than current version. "
|
||||
+ "current version: " + this.version + ", new version: " + version + ".")
|
||||
}
|
||||
this.version = version
|
||||
statements = ArrayList()
|
||||
statementsMap.put(version, statements)
|
||||
return this
|
||||
}
|
||||
/**
|
||||
* Creates a table.
|
||||
*/
|
||||
/**
|
||||
* Creates a table with int [.COLUMN_ID] primary key.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun createTable(
|
||||
table: String,
|
||||
column: String = COLUMN_ID,
|
||||
clazz: Class<*>? = Int::class.javaPrimitiveType
|
||||
): MSQLiteBuilder {
|
||||
return statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);")
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table.
|
||||
*/
|
||||
fun dropTable(table: String): MSQLiteBuilder {
|
||||
return statement("DROP TABLE $table;")
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a column to the table.
|
||||
*/
|
||||
fun insertColumn(table: String, column: String, clazz: Class<*>?): MSQLiteBuilder {
|
||||
return statement(
|
||||
"ALTER TABLE $table ADD COLUMN $column " + javaTypeToSQLiteType(
|
||||
clazz
|
||||
) + ";"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a statement.
|
||||
*/
|
||||
fun statement(statement: String): MSQLiteBuilder {
|
||||
check(!(version == 0 || statements == null)) { "Call version() first!" }
|
||||
statements!!.add(statement)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SQLiteOpenHelper from it.
|
||||
*/
|
||||
fun build(context: Context?, name: String?, version: Int): SQLiteOpenHelper {
|
||||
return MSQLiteOpenHelper(context, name, version, this)
|
||||
}
|
||||
|
||||
fun getStatements(oldVersion: Int, newVersion: Int): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
for (i in oldVersion + 1..newVersion) {
|
||||
val list = statementsMap[i]
|
||||
if (list != null) {
|
||||
result.addAll(list)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.database;
|
||||
|
||||
/*
|
||||
* Created by Hippo on 2017/9/4.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.hippo.util.SqlUtils;
|
||||
|
||||
class MSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private final int version;
|
||||
private final MSQLiteBuilder builder;
|
||||
|
||||
public MSQLiteOpenHelper(Context context, String name, int version, MSQLiteBuilder builder) {
|
||||
super(context, name, null, version);
|
||||
this.version = version;
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
for (String command : builder.getStatements(oldVersion, newVersion)) {
|
||||
db.execSQL(command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
SqlUtils.dropAllTable(db);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2017 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import com.hippo.util.SqlUtils
|
||||
|
||||
internal class MSQLiteOpenHelper(
|
||||
context: Context?,
|
||||
name: String?,
|
||||
private val version: Int,
|
||||
private val builder: MSQLiteBuilder
|
||||
) : SQLiteOpenHelper(context, name, null, version) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
onUpgrade(db, 0, version)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
for (command in builder.getStatements(oldVersion, newVersion)) {
|
||||
db.execSQL(command)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
SqlUtils.dropAllTable(db)
|
||||
onCreate(db)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import com.hippo.conaco.ConacoTask;
|
||||
import com.hippo.conaco.Unikery;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.image.ImageDrawable;
|
||||
import com.hippo.image.RecycledException;
|
||||
import com.hippo.widget.ObservedTextView;
|
||||
|
||||
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
|
||||
@@ -127,7 +126,7 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
|
||||
ImageDrawable drawable;
|
||||
try {
|
||||
drawable = new ImageDrawable(value);
|
||||
} catch (RecycledException e) {
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "The ImageBitmap is recycled", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,20 +31,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongSparseArray;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
/**
|
||||
* Add setChoiceMode for RecyclerView
|
||||
*/
|
||||
// Get some code from twoway-view and AbsListView.
|
||||
public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
|
||||
/**
|
||||
* Represents an invalid position. All valid positions are in the range 0 to 1 less than the
|
||||
* number of items in the current adapter.
|
||||
*/
|
||||
public static final int INVALID_POSITION = -1;
|
||||
public class EasyRecyclerView extends RecyclerView {
|
||||
|
||||
/**
|
||||
* Normal list that does not indicate choices
|
||||
@@ -70,23 +63,21 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
* The list allows multiple choices in custom action
|
||||
*/
|
||||
public static final int CHOICE_MODE_MULTIPLE_CUSTOM = 4;
|
||||
|
||||
/**
|
||||
* Controls if/how the user may choose/check items in the list
|
||||
*/
|
||||
private int mChoiceMode = CHOICE_MODE_NONE;
|
||||
|
||||
/**
|
||||
* Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
|
||||
*/
|
||||
private ActionMode mChoiceActionMode;
|
||||
|
||||
// Fling friction
|
||||
public float pageScrollThreshold;
|
||||
/**
|
||||
* Wrapper for the multiple choice mode callback; AbsListView needs to perform
|
||||
* a few extra actions around what application code does.
|
||||
*/
|
||||
MultiChoiceModeWrapper mMultiChoiceModeCallback;
|
||||
|
||||
/**
|
||||
* Controls if/how the user may choose/check items in the list
|
||||
*/
|
||||
private int mChoiceMode = CHOICE_MODE_NONE;
|
||||
/**
|
||||
* Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
|
||||
*/
|
||||
private ActionMode mChoiceActionMode;
|
||||
/**
|
||||
* Listener for custom multiple choices
|
||||
*/
|
||||
@@ -122,17 +113,25 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
|
||||
public EasyRecyclerView(Context context) {
|
||||
super(context);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
EasyRecyclerView.this.init(context);
|
||||
}
|
||||
|
||||
public EasyRecyclerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
EasyRecyclerView.this.init(context);
|
||||
}
|
||||
|
||||
public EasyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
EasyRecyclerView.this.init(context);
|
||||
}
|
||||
|
||||
public static void setViewChecked(View view, boolean checked) {
|
||||
if (view instanceof Checkable) {
|
||||
((Checkable) view).setChecked(checked);
|
||||
} else {
|
||||
view.setActivated(checked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,18 +171,7 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of items currently selected. This will only be valid
|
||||
* if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
|
||||
*
|
||||
* <p>To determine the specific items that are currently selected, use one of
|
||||
* the <code>getChecked*</code> methods.
|
||||
*
|
||||
* @return The number of items currently selected
|
||||
* @see #getCheckedItemPosition()
|
||||
* @see #getCheckedItemPositions()
|
||||
* @see #getCheckedItemIds()
|
||||
*/
|
||||
|
||||
public int getCheckedItemCount() {
|
||||
return mCheckedItemCount;
|
||||
}
|
||||
@@ -207,22 +195,6 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently checked item. The result is only valid if the choice
|
||||
* mode has been set to {@link #CHOICE_MODE_SINGLE}.
|
||||
*
|
||||
* @return The position of the currently checked item or
|
||||
* {@link #INVALID_POSITION} if nothing is selected
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public int getCheckedItemPosition() {
|
||||
if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
|
||||
return mCheckStates.keyAt(0);
|
||||
}
|
||||
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of checked items in the list. The result is only valid if
|
||||
* the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
|
||||
@@ -239,30 +211,6 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of checked items ids. The result is only valid if the
|
||||
* choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
|
||||
* has stable IDs. ({@link android.widget.ListAdapter#hasStableIds()} == {@code true})
|
||||
*
|
||||
* @return A new array which contains the id of each checked item in the
|
||||
* list.
|
||||
*/
|
||||
public long[] getCheckedItemIds() {
|
||||
if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) {
|
||||
return new long[0];
|
||||
}
|
||||
|
||||
final LongSparseArray<Integer> idStates = mCheckedIdStates;
|
||||
final int count = idStates.size();
|
||||
final long[] ids = new long[count];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = idStates.keyAt(i);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean isInCustomChoice() {
|
||||
return mCustomChoice;
|
||||
}
|
||||
@@ -445,14 +393,6 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
updateOnScreenCheckedViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current choice mode
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public int getChoiceMode() {
|
||||
return mChoiceMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the choice behavior for the List. By default, Lists do not have any choice behavior
|
||||
* ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
|
||||
@@ -488,24 +428,6 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the
|
||||
* selection {@link android.view.ActionMode}. Only used when the choice mode is set to
|
||||
* {@link #CHOICE_MODE_MULTIPLE_MODAL}.
|
||||
*
|
||||
* @param listener Listener that will manage the selection mode
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
||||
if (mMultiChoiceModeCallback == null) {
|
||||
mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
|
||||
}
|
||||
mMultiChoiceModeCallback.setWrapped(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listener
|
||||
*/
|
||||
public void setCustomCheckedListener(CustomChoiceListener listener) {
|
||||
mCustomChoiceListener = listener;
|
||||
}
|
||||
@@ -524,12 +446,79 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setViewChecked(View view, boolean checked) {
|
||||
if (view instanceof Checkable) {
|
||||
((Checkable) view).setChecked(checked);
|
||||
} else {
|
||||
view.setActivated(checked);
|
||||
private void init(Context context) {
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
// g (m/s^2)
|
||||
// inch/meter
|
||||
// A context-specific coefficient adjusted to physical values.
|
||||
// look and feel tuning
|
||||
pageScrollThreshold = scale * 80;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final SavedState ss = new SavedState(super.onSaveInstanceState());
|
||||
|
||||
ss.choiceMode = mChoiceMode;
|
||||
ss.customChoice = mCustomChoice;
|
||||
ss.checkedItemCount = mCheckedItemCount;
|
||||
ss.checkState = mCheckStates;
|
||||
ss.checkIdState = mCheckedIdStates;
|
||||
|
||||
return ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
SavedState ss = (SavedState) state;
|
||||
super.onRestoreInstanceState(ss.getSuperState());
|
||||
|
||||
setChoiceMode(ss.choiceMode);
|
||||
mCustomChoice = ss.customChoice;
|
||||
mCheckedItemCount = ss.checkedItemCount;
|
||||
if (ss.checkState != null) {
|
||||
mCheckStates = ss.checkState;
|
||||
}
|
||||
if (ss.checkIdState != null) {
|
||||
mCheckedIdStates = ss.checkIdState;
|
||||
}
|
||||
|
||||
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mCheckedItemCount > 0) {
|
||||
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
|
||||
}
|
||||
updateOnScreenCheckedViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* A MultiChoiceModeListener receives events for {@link android.widget.AbsListView#CHOICE_MODE_MULTIPLE_MODAL}.
|
||||
* It acts as the {@link android.view.ActionMode.Callback} for the selection mode and also receives
|
||||
* {@link #onItemCheckedStateChanged(android.view.ActionMode, int, long, boolean)} events when the user
|
||||
* selects and deselects list items.
|
||||
*/
|
||||
public interface MultiChoiceModeListener extends ActionMode.Callback {
|
||||
/**
|
||||
* Called when an item is checked or unchecked during selection mode.
|
||||
*
|
||||
* @param mode The {@link android.view.ActionMode} providing the selection mode
|
||||
* @param position Adapter position of the item that was checked or unchecked
|
||||
* @param id Adapter ID of the item that was checked or unchecked
|
||||
* @param checked <code>true</code> if the item is now checked, <code>false</code>
|
||||
* if the item is now unchecked.
|
||||
*/
|
||||
void onItemCheckedStateChanged(ActionMode mode,
|
||||
int position, long id, boolean checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom checked
|
||||
*/
|
||||
public interface CustomChoiceListener {
|
||||
|
||||
void onIntoCustomChoice(EasyRecyclerView view);
|
||||
|
||||
void onOutOfCustomChoice(EasyRecyclerView view);
|
||||
|
||||
void onItemCheckedStateChanged(EasyRecyclerView view, int position, long id, boolean checked);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -549,13 +538,23 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
|
||||
public static final SavedState EMPTY_STATE = new SavedState() {
|
||||
};
|
||||
public static final Parcelable.Creator<SavedState> CREATOR
|
||||
= new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
int choiceMode;
|
||||
boolean customChoice;
|
||||
int checkedItemCount;
|
||||
SparseBooleanArray checkState;
|
||||
LongSparseArray<Integer> checkIdState;
|
||||
|
||||
// This keeps the parent(RecyclerView)'s state
|
||||
Parcelable mSuperState;
|
||||
|
||||
@@ -618,81 +617,11 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
public Parcelable getSuperState() {
|
||||
return mSuperState;
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<SavedState> CREATOR
|
||||
= new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final SavedState ss = new SavedState(super.onSaveInstanceState());
|
||||
|
||||
ss.choiceMode = mChoiceMode;
|
||||
ss.customChoice = mCustomChoice;
|
||||
ss.checkedItemCount = mCheckedItemCount;
|
||||
ss.checkState = mCheckStates;
|
||||
ss.checkIdState = mCheckedIdStates;
|
||||
|
||||
return ss;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
SavedState ss = (SavedState) state;
|
||||
super.onRestoreInstanceState(ss.getSuperState());
|
||||
|
||||
setChoiceMode(ss.choiceMode);
|
||||
mCustomChoice = ss.customChoice;
|
||||
mCheckedItemCount = ss.checkedItemCount;
|
||||
if (ss.checkState != null) {
|
||||
mCheckStates = ss.checkState;
|
||||
}
|
||||
if (ss.checkIdState != null) {
|
||||
mCheckedIdStates = ss.checkIdState;
|
||||
}
|
||||
|
||||
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mCheckedItemCount > 0) {
|
||||
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
|
||||
}
|
||||
updateOnScreenCheckedViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* A MultiChoiceModeListener receives events for {@link android.widget.AbsListView#CHOICE_MODE_MULTIPLE_MODAL}.
|
||||
* It acts as the {@link android.view.ActionMode.Callback} for the selection mode and also receives
|
||||
* {@link #onItemCheckedStateChanged(android.view.ActionMode, int, long, boolean)} events when the user
|
||||
* selects and deselects list items.
|
||||
*/
|
||||
public interface MultiChoiceModeListener extends ActionMode.Callback {
|
||||
/**
|
||||
* Called when an item is checked or unchecked during selection mode.
|
||||
*
|
||||
* @param mode The {@link android.view.ActionMode} providing the selection mode
|
||||
* @param position Adapter position of the item that was checked or unchecked
|
||||
* @param id Adapter ID of the item that was checked or unchecked
|
||||
* @param checked <code>true</code> if the item is now checked, <code>false</code>
|
||||
* if the item is now unchecked.
|
||||
*/
|
||||
void onItemCheckedStateChanged(ActionMode mode,
|
||||
int position, long id, boolean checked);
|
||||
}
|
||||
|
||||
class MultiChoiceModeWrapper implements MultiChoiceModeListener {
|
||||
private MultiChoiceModeListener mWrapped;
|
||||
|
||||
public void setWrapped(MultiChoiceModeListener wrapped) {
|
||||
mWrapped = wrapped;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean hasWrappedCallback() {
|
||||
@@ -743,16 +672,4 @@ public class EasyRecyclerView extends ScrollLessRecyclerView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom checked
|
||||
*/
|
||||
public interface CustomChoiceListener {
|
||||
|
||||
void onIntoCustomChoice(EasyRecyclerView view);
|
||||
|
||||
void onOutOfCustomChoice(EasyRecyclerView view);
|
||||
|
||||
void onItemCheckedStateChanged(EasyRecyclerView view, int position, long id, boolean checked);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,9 @@ import androidx.annotation.NonNull;
|
||||
|
||||
public class HandlerDrawable extends Drawable {
|
||||
|
||||
private int mColor = Color.BLACK;
|
||||
private final Paint mPaint;
|
||||
private final RectF mTemp = new RectF();
|
||||
private int mColor = Color.BLACK;
|
||||
|
||||
public HandlerDrawable() {
|
||||
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.easyrecyclerview;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
// Add overlap
|
||||
|
||||
/**
|
||||
* Only work for {@link androidx.recyclerview.widget.LinearLayoutManager}.<br>
|
||||
* Show divider between item, just like
|
||||
* {@link android.widget.ListView#setDivider(android.graphics.drawable.Drawable)}
|
||||
*/
|
||||
public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||
public static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;
|
||||
public static final int VERTICAL = LinearLayoutManager.VERTICAL;
|
||||
|
||||
private boolean mShowFirstDivider = false;
|
||||
private boolean mShowLastDivider = false;
|
||||
|
||||
private final Rect mRect;
|
||||
private final Paint mPaint;
|
||||
|
||||
private int mOrientation;
|
||||
private int mThickness;
|
||||
private int mPaddingStart = 0;
|
||||
private int mPaddingEnd = 0;
|
||||
|
||||
private boolean mOverlap = false;
|
||||
|
||||
private ShowDividerHelper mShowDividerHelper;
|
||||
|
||||
public LinearDividerItemDecoration(int orientation, int color, int thickness) {
|
||||
mRect = new Rect();
|
||||
mPaint = new Paint();
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
setOrientation(orientation);
|
||||
setColor(color);
|
||||
setThickness(thickness);
|
||||
}
|
||||
|
||||
public void setShowDividerHelper(ShowDividerHelper showDividerHelper) {
|
||||
mShowDividerHelper = showDividerHelper;
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
if (orientation != HORIZONTAL && orientation != VERTICAL) {
|
||||
throw new IllegalArgumentException("invalid orientation");
|
||||
}
|
||||
mOrientation = orientation;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
mPaint.setColor(color);
|
||||
}
|
||||
|
||||
public void setThickness(int thickness) {
|
||||
mThickness = thickness;
|
||||
}
|
||||
|
||||
public void setShowFirstDivider(boolean showFirstDivider) {
|
||||
mShowFirstDivider = showFirstDivider;
|
||||
}
|
||||
|
||||
public void setShowLastDivider(boolean showLastDivider) {
|
||||
mShowLastDivider = showLastDivider;
|
||||
}
|
||||
|
||||
public void setPadding(int padding) {
|
||||
setPaddingStart(padding);
|
||||
setPaddingEnd(padding);
|
||||
}
|
||||
|
||||
public void setPaddingStart(int paddingStart) {
|
||||
mPaddingStart = paddingStart;
|
||||
}
|
||||
|
||||
public void setPaddingEnd(int paddingEnd) {
|
||||
mPaddingEnd = paddingEnd;
|
||||
}
|
||||
|
||||
public void setOverlap(boolean overlap) {
|
||||
mOverlap = overlap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view,
|
||||
RecyclerView parent, RecyclerView.State state) {
|
||||
if (parent.getAdapter() == null) {
|
||||
outRect.set(0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOverlap) {
|
||||
outRect.set(0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
final int position = parent.getChildLayoutPosition(view);
|
||||
final int itemCount = parent.getAdapter().getItemCount();
|
||||
|
||||
if (mShowDividerHelper != null) {
|
||||
if (mOrientation == VERTICAL) {
|
||||
if (position == 0 && mShowDividerHelper.showDivider(0)) {
|
||||
outRect.top = mThickness;
|
||||
}
|
||||
if (mShowDividerHelper.showDivider(position + 1)) {
|
||||
outRect.bottom = mThickness;
|
||||
}
|
||||
} else {
|
||||
if (position == 0 && mShowDividerHelper.showDivider(0)) {
|
||||
outRect.left = mThickness;
|
||||
}
|
||||
if (mShowDividerHelper.showDivider(position + 1)) {
|
||||
outRect.right = mThickness;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mOrientation == VERTICAL) {
|
||||
if (position == 0 && mShowFirstDivider) {
|
||||
outRect.top = mThickness;
|
||||
}
|
||||
outRect.bottom = mThickness;
|
||||
if (position == itemCount - 1 && !mShowLastDivider) {
|
||||
outRect.bottom = 0;
|
||||
}
|
||||
} else {
|
||||
if (position == 0 && mShowFirstDivider) {
|
||||
outRect.left = mThickness;
|
||||
}
|
||||
outRect.right = mThickness;
|
||||
if (position == itemCount - 1 && !mShowLastDivider) {
|
||||
outRect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(Canvas c, RecyclerView parent,
|
||||
RecyclerView.State state) {
|
||||
RecyclerView.Adapter adapter = parent.getAdapter();
|
||||
if (adapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int itemCount = adapter.getItemCount();
|
||||
boolean overlap = mOverlap;
|
||||
|
||||
if (mOrientation == VERTICAL) {
|
||||
final boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
int mPaddingLeft;
|
||||
int mPaddingRight;
|
||||
if (isRtl) {
|
||||
mPaddingLeft = mPaddingEnd;
|
||||
mPaddingRight = mPaddingStart;
|
||||
} else {
|
||||
mPaddingLeft = mPaddingStart;
|
||||
mPaddingRight = mPaddingEnd;
|
||||
}
|
||||
|
||||
final int left = parent.getPaddingLeft() + mPaddingLeft;
|
||||
final int right = parent.getWidth() - parent.getPaddingRight() - mPaddingRight;
|
||||
final int childCount = parent.getChildCount();
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
final int position = parent.getChildLayoutPosition(child);
|
||||
|
||||
boolean show;
|
||||
if (mShowDividerHelper != null) {
|
||||
show = mShowDividerHelper.showDivider(position + 1);
|
||||
} else {
|
||||
show = position != itemCount - 1 || mShowLastDivider;
|
||||
}
|
||||
if (show) {
|
||||
int top = child.getBottom() + lp.bottomMargin;
|
||||
if (overlap) {
|
||||
top -= mThickness;
|
||||
}
|
||||
final int bottom = top + mThickness;
|
||||
mRect.set(left, top, right, bottom);
|
||||
c.drawRect(mRect, mPaint);
|
||||
}
|
||||
|
||||
if (position == 0) {
|
||||
if (mShowDividerHelper != null) {
|
||||
show = mShowDividerHelper.showDivider(0);
|
||||
} else {
|
||||
show = mShowFirstDivider;
|
||||
}
|
||||
if (show) {
|
||||
int bottom = child.getTop() + lp.topMargin;
|
||||
if (overlap) {
|
||||
bottom += mThickness;
|
||||
}
|
||||
final int top = bottom - mThickness;
|
||||
mRect.set(left, top, right, bottom);
|
||||
c.drawRect(mRect, mPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final int top = parent.getPaddingTop() + mPaddingStart;
|
||||
final int bottom = parent.getHeight() - parent.getPaddingBottom() - mPaddingEnd;
|
||||
final int childCount = parent.getChildCount();
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
final int position = parent.getChildLayoutPosition(child);
|
||||
|
||||
boolean show;
|
||||
if (mShowDividerHelper != null) {
|
||||
show = mShowDividerHelper.showDivider(position + 1);
|
||||
} else {
|
||||
show = position != itemCount - 1 || mShowLastDivider;
|
||||
}
|
||||
if (show) {
|
||||
int left = child.getRight() + lp.rightMargin;
|
||||
if (overlap) {
|
||||
left -= mThickness;
|
||||
}
|
||||
final int right = left + mThickness;
|
||||
mRect.set(left, top, right, bottom);
|
||||
c.drawRect(mRect, mPaint);
|
||||
}
|
||||
|
||||
if (position == 0) {
|
||||
if (mShowDividerHelper != null) {
|
||||
show = mShowDividerHelper.showDivider(0);
|
||||
} else {
|
||||
show = mShowFirstDivider;
|
||||
}
|
||||
if (show) {
|
||||
int right = child.getLeft() + lp.leftMargin;
|
||||
if (overlap) {
|
||||
right += mThickness;
|
||||
}
|
||||
final int left = right - mThickness;
|
||||
mRect.set(left, top, right, bottom);
|
||||
c.drawRect(mRect, mPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ShowDividerHelper {
|
||||
boolean showDivider(int index);
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
package com.hippo.easyrecyclerview;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.BridgeRecyclerView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
public class ScrollLessRecyclerView extends BridgeRecyclerView {
|
||||
|
||||
private boolean isScrollBefore = false;
|
||||
private int initialTouchX;
|
||||
private int initialTouchY;
|
||||
|
||||
// A context-specific coefficient adjusted to physical values.
|
||||
private float mPhysicalCoeff;
|
||||
// Fling friction
|
||||
private final float mFlingFriction = ViewConfiguration.getScrollFriction();
|
||||
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
|
||||
|
||||
public float pageScrollThreshold;
|
||||
private int defaultOverflowItemScrollKeep;
|
||||
|
||||
private boolean enableScroll = false;
|
||||
|
||||
public ScrollLessRecyclerView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScrollLessRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScrollLessRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
final float ppi = scale * 160.0f;
|
||||
mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
|
||||
* 39.37f // inch/meter
|
||||
* ppi
|
||||
* 0.84f; // look and feel tuning
|
||||
pageScrollThreshold = scale * 80;
|
||||
}
|
||||
|
||||
public void setPageScrollThreshold(float pageScrollThreshold) {
|
||||
this.pageScrollThreshold = pageScrollThreshold;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
if (enableScroll) {
|
||||
return super.onTouchEvent(e);
|
||||
}
|
||||
boolean superTouchResult = super.onTouchEvent(e);
|
||||
if (superTouchResult) {
|
||||
switch (e.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
initialTouchX = (int) (e.getX() + 0.5f);
|
||||
initialTouchY = (int) (e.getY() + 0.5f);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (initialTouchX < 0 || initialTouchY < 0) {
|
||||
initialTouchX = (int) (e.getX() + 0.5f);
|
||||
initialTouchY = (int) (e.getY() + 0.5f);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (isScrollBefore) {
|
||||
int moveDistanceX = (int) (initialTouchX - e.getX() + flingMoveDistanceX);
|
||||
int moveDistanceY = (int) (initialTouchY - e.getY() + flingMoveDistanceY);
|
||||
onTouchScroll(moveDistanceX, moveDistanceY);
|
||||
initialTouchX = initialTouchY = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
isScrollBefore = getScrollState() == SCROLL_STATE_DRAGGING;
|
||||
}
|
||||
return superTouchResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
|
||||
if (enableScroll) {
|
||||
return super.scrollByInternal(x, y, ev, type);
|
||||
}
|
||||
//disable drag scroll effect,e-ink screen don't need it
|
||||
return false;
|
||||
}
|
||||
|
||||
double flingMoveDistanceX;
|
||||
double flingMoveDistanceY;
|
||||
|
||||
@Override
|
||||
public boolean fling(int velocityX, int velocityY) {
|
||||
if (enableScroll) {
|
||||
return super.fling(velocityX, velocityY);
|
||||
}
|
||||
flingMoveDistanceX = getSplineFlingDistance(velocityX) * Math.signum(velocityX);
|
||||
flingMoveDistanceY = getSplineFlingDistance(velocityY) * Math.signum(velocityY);
|
||||
//disable fling scroll effect,e-ink screen don't need it
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canScrollVertical = true;
|
||||
boolean canScrollHorizontally = true;
|
||||
|
||||
protected void onTouchScroll(int dx, int dy) {
|
||||
canScrollVertical = true;
|
||||
canScrollHorizontally = true;
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout != null) {
|
||||
if (dy > pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
canScrollVertical = false;
|
||||
onScrollDown();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
canScrollHorizontally = false;
|
||||
onScrollRight();
|
||||
}
|
||||
} else if (dy < -pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
canScrollVertical = false;
|
||||
onScrollUp();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
canScrollHorizontally = false;
|
||||
onScrollLeft();
|
||||
}
|
||||
}
|
||||
|
||||
if (dx > pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
onScrollDown();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
onScrollRight();
|
||||
}
|
||||
} else if (dx < -pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
onScrollUp();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
onScrollLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollLeft() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int firstVisiblePosition = realLayout.findFirstVisibleItemPosition();
|
||||
ViewHolder firstVisibleItem = findViewHolderForAdapterPosition(firstVisiblePosition);
|
||||
|
||||
if (firstVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item width overscreen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (firstVisibleItem.itemView.getMeasuredWidth() > getMeasuredWidth()) {
|
||||
super.scrollByInternal(-getMeasuredWidth() + defaultOverflowItemScrollKeep, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (firstVisibleItem.itemView.getMeasuredWidth() == getMeasuredWidth()) {
|
||||
super.scrollByInternal(-getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(firstVisibleItem.itemView.getRight() - getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] firstVisiblePosition = realLayout.findFirstVisibleItemPositions(null);
|
||||
ViewHolder[] firstVisibleItems = new ViewHolder[firstVisiblePosition.length];
|
||||
for (int i = 0; i < firstVisiblePosition.length; i++) {
|
||||
firstVisibleItems[i] = findViewHolderForAdapterPosition(firstVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = Integer.MAX_VALUE;
|
||||
for (ViewHolder firstVisibleItem : firstVisibleItems) {
|
||||
if (firstVisibleItem != null) {
|
||||
if (firstVisibleItem.itemView.getRight() < scrollDistance) {
|
||||
scrollDistance = firstVisibleItem.itemView.getRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollDistance == getMeasuredWidth()) {
|
||||
scrollDistance = 0;
|
||||
}
|
||||
super.scrollByInternal(scrollDistance - getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollRight() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int lastVisiblePosition = realLayout.findLastVisibleItemPosition();
|
||||
ViewHolder lastVisibleItem = findViewHolderForAdapterPosition(lastVisiblePosition);
|
||||
|
||||
if (lastVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item width overflow screen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (lastVisibleItem.itemView.getMeasuredWidth() > getMeasuredWidth()) {
|
||||
super.scrollByInternal(getMeasuredWidth() - defaultOverflowItemScrollKeep, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (lastVisibleItem.itemView.getMeasuredWidth() == getMeasuredWidth()) {
|
||||
super.scrollByInternal(getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(lastVisibleItem.itemView.getLeft(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] lastVisiblePosition = realLayout.findLastVisibleItemPositions(null);
|
||||
ViewHolder[] lastVisibleItems = new ViewHolder[lastVisiblePosition.length];
|
||||
for (int i = 0; i < lastVisiblePosition.length; i++) {
|
||||
lastVisibleItems[i] = findViewHolderForAdapterPosition(lastVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = 0;
|
||||
for (ViewHolder lastVisibleItem : lastVisibleItems) {
|
||||
if (lastVisibleItem != null) {
|
||||
if (lastVisibleItem.itemView.getLeft() > scrollDistance) {
|
||||
scrollDistance = lastVisibleItem.itemView.getLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == 0) {
|
||||
scrollDistance = getMeasuredWidth();
|
||||
}
|
||||
super.scrollByInternal(scrollDistance, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollUp() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int firstVisiblePosition = realLayout.findFirstVisibleItemPosition();
|
||||
ViewHolder firstVisibleItem = findViewHolderForAdapterPosition(firstVisiblePosition);
|
||||
|
||||
if (firstVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item height overscreen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (firstVisibleItem.itemView.getMeasuredHeight() > getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, -getMeasuredHeight() + defaultOverflowItemScrollKeep, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (firstVisibleItem.itemView.getMeasuredHeight() == getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, -getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(0, firstVisibleItem.itemView.getBottom() - getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] firstVisiblePosition = realLayout.findFirstVisibleItemPositions(null);
|
||||
ViewHolder[] firstVisibleItems = new ViewHolder[firstVisiblePosition.length];
|
||||
for (int i = 0; i < firstVisiblePosition.length; i++) {
|
||||
firstVisibleItems[i] = findViewHolderForAdapterPosition(firstVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = Integer.MAX_VALUE;
|
||||
for (ViewHolder firstVisibleItem : firstVisibleItems) {
|
||||
if (firstVisibleItem != null) {
|
||||
if (firstVisibleItem.itemView.getBottom() < scrollDistance) {
|
||||
scrollDistance = firstVisibleItem.itemView.getBaseline();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == getMeasuredWidth()) {
|
||||
scrollDistance = 0;
|
||||
}
|
||||
super.scrollByInternal(0, scrollDistance - getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollDown() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int lastVisiblePosition = realLayout.findLastVisibleItemPosition();
|
||||
ViewHolder lastVisibleItem = findViewHolderForAdapterPosition(lastVisiblePosition);
|
||||
|
||||
if (lastVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item height overflow screen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (lastVisibleItem.itemView.getMeasuredHeight() > getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, getMeasuredHeight() - defaultOverflowItemScrollKeep, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (lastVisibleItem.itemView.getMeasuredHeight() == getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(0, lastVisibleItem.itemView.getTop(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] lastVisiblePosition = realLayout.findLastVisibleItemPositions(null);
|
||||
ViewHolder[] lastVisibleItems = new ViewHolder[lastVisiblePosition.length];
|
||||
for (int i = 0; i < lastVisiblePosition.length; i++) {
|
||||
lastVisibleItems[i] = findViewHolderForAdapterPosition(lastVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = 0;
|
||||
for (ViewHolder lastVisibleItem : lastVisibleItems) {
|
||||
if (lastVisibleItem != null) {
|
||||
if (lastVisibleItem.itemView.getTop() > scrollDistance) {
|
||||
scrollDistance = lastVisibleItem.itemView.getTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == 0) {
|
||||
scrollDistance = getMeasuredHeight();
|
||||
}
|
||||
super.scrollByInternal(0, scrollDistance, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
defaultOverflowItemScrollKeep = (int) (h * 0.05f);
|
||||
}
|
||||
|
||||
private double getSplineDeceleration(int velocity) {
|
||||
return Math.log(0.35f * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
|
||||
}
|
||||
|
||||
private double getSplineFlingDistance(int velocity) {
|
||||
final double l = getSplineDeceleration(velocity);
|
||||
final double decelMinusOne = DECELERATION_RATE - 1.0;
|
||||
return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
|
||||
}
|
||||
|
||||
public void setEnableScroll(boolean enableScroll) {
|
||||
this.enableScroll = enableScroll;
|
||||
}
|
||||
|
||||
public boolean isEnableScroll() {
|
||||
return enableScroll;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.hippo.scene.SceneFragment;
|
||||
import com.microsoft.appcenter.AppCenter;
|
||||
import com.microsoft.appcenter.crashes.Crashes;
|
||||
import com.microsoft.appcenter.distribute.Distribute;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class Analytics {
|
||||
|
||||
public static void start(Application application) {
|
||||
AppCenter.start(application, "1aa06805-037c-4f3c-8aaa-e23d1433986d", com.microsoft.appcenter.analytics.Analytics.class, Crashes.class, Distribute.class);
|
||||
AppCenter.setUserId(Settings.getUserID());
|
||||
com.microsoft.appcenter.analytics.Analytics.setEnabled(Settings.getEnableAnalytics());
|
||||
Crashes.setEnabled(!BuildConfig.DEBUG);
|
||||
Distribute.setEnabledForDebuggableBuild(false);
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return AppCenter.isConfigured() && Settings.getEnableAnalytics();
|
||||
}
|
||||
|
||||
public static void setEnabled(boolean enabled) {
|
||||
com.microsoft.appcenter.analytics.Analytics.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public static void onSceneView(SceneFragment scene) {
|
||||
if (isEnabled()) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put("scene_simple_class", scene.getClass().getSimpleName());
|
||||
properties.put("scene_class", scene.getClass().getName());
|
||||
com.microsoft.appcenter.analytics.Analytics.trackEvent("scene_view", properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Debug;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -35,8 +36,14 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.hippo.Native;
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
import com.hippo.conaco.Conaco;
|
||||
@@ -57,7 +64,6 @@ import com.hippo.image.Image;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.network.StatusCodeException;
|
||||
import com.hippo.scene.SceneApplication;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.BitmapUtils;
|
||||
import com.hippo.util.ClipboardUtil;
|
||||
@@ -90,8 +96,6 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
import rikka.material.app.LocaleDelegate;
|
||||
|
||||
public class EhApplication extends SceneApplication {
|
||||
|
||||
@@ -103,6 +107,8 @@ public class EhApplication extends SceneApplication {
|
||||
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
|
||||
private static final long DEBUG_PRINT_INTERVAL = 3000L;
|
||||
|
||||
public static boolean locked = false;
|
||||
|
||||
private static EhApplication instance;
|
||||
|
||||
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
|
||||
@@ -296,14 +302,10 @@ public class EhApplication extends SceneApplication {
|
||||
return application.mFavouriteStatusRouter;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getDeveloperEmail() {
|
||||
return "nekoworkshop$protonmail.com".replace('$', '@');
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
|
||||
instance = this;
|
||||
|
||||
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
@@ -329,7 +331,6 @@ public class EhApplication extends SceneApplication {
|
||||
StatusCodeException.initialize(this);
|
||||
Settings.initialize(this);
|
||||
ReadableTime.initialize(this);
|
||||
Html.initialize(this);
|
||||
AppConfig.initialize(this);
|
||||
SpiderDen.initialize(this);
|
||||
EhDB.initialize(this);
|
||||
@@ -340,11 +341,8 @@ public class EhApplication extends SceneApplication {
|
||||
EhDB.mergeOldDB(this);
|
||||
}
|
||||
|
||||
Analytics.start(this);
|
||||
|
||||
LocaleDelegate.setDefaultLocale(Settings.getLocale());
|
||||
DayNightDelegate.setApplicationContext(this);
|
||||
DayNightDelegate.setDefaultNightMode(Settings.getTheme());
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
|
||||
|
||||
// Do io tasks in new thread
|
||||
//noinspection deprecation
|
||||
@@ -431,7 +429,7 @@ public class EhApplication extends SceneApplication {
|
||||
Activity activity = getTopActivity();
|
||||
if (activity != null) {
|
||||
activity.runOnUiThread(() -> {
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(Html.fromHtml(html))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create();
|
||||
@@ -588,4 +586,11 @@ public class EhApplication extends SceneApplication {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class EhlifecycleObserver implements DefaultLifecycleObserver {
|
||||
@Override
|
||||
public void onPause(@NonNull LifecycleOwner owner) {
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class Settings {
|
||||
****** Advanced
|
||||
********************/
|
||||
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
|
||||
public static final String KEY_SECURITY = "security";
|
||||
public static final String KEY_SECURITY = "require_unlock";
|
||||
public static final String DEFAULT_SECURITY = "";
|
||||
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
|
||||
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
|
||||
@@ -457,14 +457,6 @@ public class Settings {
|
||||
putBoolean(KEY_SELECT_SITE, value);
|
||||
}
|
||||
|
||||
public static boolean getQuickSearchTip() {
|
||||
return getBoolean(KEY_QUICK_SEARCH_TIP, DEFAULT_QUICK_SEARCH_TIP);
|
||||
}
|
||||
|
||||
public static void putQuickSearchTip(boolean value) {
|
||||
putBoolean(KEY_QUICK_SEARCH_TIP, value);
|
||||
}
|
||||
|
||||
public static int getTheme() {
|
||||
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
|
||||
}
|
||||
@@ -594,14 +586,6 @@ public class Settings {
|
||||
putString(KEY_EXCLUDED_LANGUAGES, value);
|
||||
}
|
||||
|
||||
public static void putEInkMode(boolean value) {
|
||||
putBoolean(KEY_E_INK_MODE, value);
|
||||
}
|
||||
|
||||
public static boolean getEInkMode() {
|
||||
return getBoolean(KEY_E_INK_MODE, DEFAULT_E_INK_MODE);
|
||||
}
|
||||
|
||||
public static boolean getMeteredNetworkWarning() {
|
||||
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
|
||||
}
|
||||
@@ -1044,22 +1028,18 @@ public class Settings {
|
||||
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
|
||||
}
|
||||
|
||||
public static String getSecurity() {
|
||||
return getString(KEY_SECURITY, DEFAULT_SECURITY);
|
||||
public static boolean getSecurity() {
|
||||
return getBoolean(KEY_SECURITY, false);
|
||||
}
|
||||
|
||||
public static void putSecurity(String value) {
|
||||
putString(KEY_SECURITY, value);
|
||||
public static void putSecurity(boolean value) {
|
||||
putBoolean(KEY_SECURITY, value);
|
||||
}
|
||||
|
||||
public static boolean getEnableFingerprint() {
|
||||
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
|
||||
}
|
||||
|
||||
public static void putEnableFingerprint(boolean value) {
|
||||
putBoolean(KEY_ENABLE_FINGERPRINT, value);
|
||||
}
|
||||
|
||||
public static int getReadCacheSize() {
|
||||
return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,11 @@ package com.hippo.ehviewer;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrlOpener;
|
||||
@@ -36,8 +34,6 @@ import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.scene.Announcer;
|
||||
import com.hippo.scene.StageActivity;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
|
||||
public final class UrlOpener {
|
||||
|
||||
private UrlOpener() {
|
||||
@@ -92,14 +88,8 @@ public final class UrlOpener {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNight = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
|
||||
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
|
||||
customTabsIntent.setShowTitle(true);
|
||||
CustomTabColorSchemeParams params = new CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(ResourcesKt.resolveColor(context.getTheme(), R.attr.toolbarColor))
|
||||
.build();
|
||||
customTabsIntent.setDefaultColorSchemeParams(params);
|
||||
customTabsIntent.setColorScheme(isNight ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT);
|
||||
try {
|
||||
customTabsIntent.build().launchUrl(context, uri);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.view.View;
|
||||
@@ -15,14 +33,13 @@ public class WindowInsetsAnimationHelper extends WindowInsetsAnimationCompat.Cal
|
||||
private final View[] views;
|
||||
private final HashMap<View, Integer> startPaddings = new HashMap<>();
|
||||
private final HashMap<View, Integer> endPaddings = new HashMap<>();
|
||||
WindowInsetsAnimationCompat animation;
|
||||
|
||||
public WindowInsetsAnimationHelper(int dispatchMode, View... views) {
|
||||
super(dispatchMode);
|
||||
this.views = views;
|
||||
}
|
||||
|
||||
WindowInsetsAnimationCompat animation;
|
||||
|
||||
@Override
|
||||
public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
|
||||
super.onPrepare(animation);
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser;
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser;
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser;
|
||||
import com.hippo.ehviewer.client.parser.ForumsParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryApiParser;
|
||||
@@ -48,7 +49,6 @@ import com.hippo.ehviewer.client.parser.GalleryTokenApiParser;
|
||||
import com.hippo.ehviewer.client.parser.ProfileParser;
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryParser;
|
||||
import com.hippo.ehviewer.client.parser.SignInParser;
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser;
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser;
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentParser;
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser;
|
||||
@@ -971,7 +971,7 @@ public class EhEngine {
|
||||
try {
|
||||
Response response = call.execute();
|
||||
|
||||
Log.d(TAG, "" + response.request().url().toString());
|
||||
Log.d(TAG, "" + response.request().url());
|
||||
|
||||
code = response.code();
|
||||
headers = response.headers();
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
@@ -56,9 +56,9 @@ import okio.Okio;
|
||||
public class EhTagDatabase {
|
||||
|
||||
private static final Map<String, String> NAMESPACE_TO_PREFIX = new HashMap<>();
|
||||
private static volatile EhTagDatabase instance;
|
||||
// TODO more lock for different language
|
||||
private static final Lock lock = new ReentrantLock();
|
||||
private static volatile EhTagDatabase instance;
|
||||
|
||||
static {
|
||||
NAMESPACE_TO_PREFIX.put("artist", "a:");
|
||||
|
||||
@@ -36,40 +36,29 @@ public class EhUrl {
|
||||
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
|
||||
|
||||
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
|
||||
public static final String HOST_E = "https://" + DOMAIN_E + "/";
|
||||
|
||||
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
|
||||
|
||||
public static final String API_E = HOST_E + "api.php";
|
||||
public static final String API_EX = HOST_EX + "api.php";
|
||||
|
||||
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
|
||||
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
|
||||
public static final String URL_WATCHED_EX = HOST_EX + "watched";
|
||||
public static final String HOST_E = "https://" + DOMAIN_E + "/";
|
||||
public static final String API_E = HOST_E + "api.php";
|
||||
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
|
||||
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
|
||||
public static final String URL_WATCHED_E = HOST_E + "watched";
|
||||
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
|
||||
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
|
||||
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
|
||||
|
||||
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
|
||||
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
|
||||
|
||||
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
|
||||
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
|
||||
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
|
||||
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
|
||||
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
|
||||
|
||||
public static final String REFERER_EX = "https://" + DOMAIN_EX;
|
||||
public static final String REFERER_E = "https://" + DOMAIN_E;
|
||||
|
||||
public static final String ORIGIN_EX = REFERER_EX;
|
||||
public static final String REFERER_E = "https://" + DOMAIN_E;
|
||||
public static final String ORIGIN_E = REFERER_E;
|
||||
|
||||
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
|
||||
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
|
||||
|
||||
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
|
||||
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
|
||||
|
||||
public static final String URL_WATCHED_E = HOST_E + "watched";
|
||||
public static final String URL_WATCHED_EX = HOST_EX + "watched";
|
||||
|
||||
private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/";
|
||||
private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/";
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
+18
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -224,8 +223,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setColorized(true)
|
||||
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
|
||||
.setShowWhen(false)
|
||||
.setChannelId(CHANNEL_ID);
|
||||
@@ -254,7 +251,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(getString(R.string.stat_download_done_title))
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setDeleteIntent(piClear)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
@@ -273,7 +269,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
.setContentText(getString(R.string.stat_509_alert_title))
|
||||
.setContentText(getString(R.string.stat_509_alert_text))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)))
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.a7zip.ArchiveException;
|
||||
import com.hippo.a7zip.InArchive;
|
||||
import com.hippo.a7zip.PropID;
|
||||
import com.hippo.a7zip.PropType;
|
||||
import com.hippo.a7zip.SeekableInputStream;
|
||||
import com.hippo.unifile.UniRandomAccessFile;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class A7ZipArchive implements Closeable {
|
||||
|
||||
private final InArchive archive;
|
||||
|
||||
private A7ZipArchive(InArchive archive) {
|
||||
this.archive = archive;
|
||||
}
|
||||
|
||||
private static boolean isSupportedFilename(String name) {
|
||||
for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
if (name.endsWith(extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static A7ZipArchive create(UniRandomAccessFile file) throws ArchiveException {
|
||||
SeekableInputStream store = new UniRandomAccessFileInStream(file);
|
||||
InArchive archive = InArchive.open(store);
|
||||
if ((archive.getArchivePropertyType(PropID.ENCRYPTED) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.ENCRYPTED))
|
||||
|| (archive.getArchivePropertyType(PropID.SOLID) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.SOLID))
|
||||
|| (archive.getArchivePropertyType(PropID.IS_VOLUME) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.IS_VOLUME))) {
|
||||
throw new ArchiveException("Unsupported archive");
|
||||
}
|
||||
return new A7ZipArchive(archive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
archive.close();
|
||||
}
|
||||
|
||||
List<A7ZipArchiveEntry> getArchiveEntries() {
|
||||
List<A7ZipArchiveEntry> entries = new ArrayList<>();
|
||||
|
||||
for (int i = 0, n = archive.getNumberOfEntries(); i < n; i++) {
|
||||
if (!archive.getEntryBooleanProperty(i, PropID.ENCRYPTED)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.IS_DIR)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.IS_VOLUME)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.SOLID)) {
|
||||
String path = archive.getEntryPath(i);
|
||||
if (isSupportedFilename(path.toLowerCase())) {
|
||||
entries.add(new A7ZipArchiveEntry(archive, i, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static class A7ZipArchiveEntry {
|
||||
|
||||
private final InArchive archive;
|
||||
private final int index;
|
||||
private final String path;
|
||||
|
||||
private A7ZipArchiveEntry(InArchive archive, int index, String path) {
|
||||
this.archive = archive;
|
||||
this.index = index;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
void extract(OutputStream os) throws ArchiveException {
|
||||
archive.extractEntry(index, new OutputStreamSequentialOutStream(os));
|
||||
}
|
||||
}
|
||||
|
||||
private static class UniRandomAccessFileInStream extends SeekableInputStream {
|
||||
|
||||
private final byte[] scratch = new byte[8];
|
||||
private final UniRandomAccessFile file;
|
||||
|
||||
public UniRandomAccessFileInStream(UniRandomAccessFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long pos) throws IOException {
|
||||
file.seek(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long tell() throws IOException {
|
||||
return file.getFilePointer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return (file.read(scratch, 0, 1) != -1) ? scratch[0] & 0xff : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] b) throws IOException {
|
||||
return file.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return file.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class OutputStreamSequentialOutStream extends OutputStream {
|
||||
|
||||
private final OutputStream stream;
|
||||
|
||||
public OutputStreamSequentialOutStream(OutputStream stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
stream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
stream.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,23 +23,18 @@ import android.os.Process;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.a7zip.ArchiveException;
|
||||
import com.hippo.UriArchiveAccessor;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.glgallery.GalleryPageView;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.unifile.UniRandomAccessFile;
|
||||
import com.hippo.util.NaturalComparator;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -47,15 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
private static final Comparator<A7ZipArchive.A7ZipArchiveEntry> naturalComparator = new Comparator<A7ZipArchive.A7ZipArchiveEntry>() {
|
||||
private final NaturalComparator comparator = new NaturalComparator();
|
||||
|
||||
@Override
|
||||
public int compare(A7ZipArchive.A7ZipArchiveEntry o1, A7ZipArchive.A7ZipArchiveEntry o2) {
|
||||
return comparator.compare(o1.getPath(), o2.getPath());
|
||||
}
|
||||
};
|
||||
private final UniFile file;
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
|
||||
private final LinkedHashMap<Integer, InputStream> streams = new LinkedHashMap<>();
|
||||
@@ -66,7 +53,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
private String error;
|
||||
|
||||
public ArchiveGalleryProvider(Context context, Uri uri) {
|
||||
file = UniFile.fromMediaUri(context, uri);
|
||||
UriArchiveAccessor archiveAccessor1;
|
||||
try {
|
||||
archiveAccessor1 = new UriArchiveAccessor(context, uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
archiveAccessor1 = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
archiveAccessor = archiveAccessor1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,37 +161,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
private class ArchiveTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
UniRandomAccessFile uraf = null;
|
||||
try {
|
||||
uraf = file.createRandomAccessFile("r");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (uraf == null) {
|
||||
size = archiveAccessor.open();
|
||||
if (size <= 0) {
|
||||
size = STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
A7ZipArchive archive = null;
|
||||
try {
|
||||
archive = A7ZipArchive.create(uraf);
|
||||
} catch (ArchiveException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (archive == null) {
|
||||
size = STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_invalid_archive);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
List<A7ZipArchive.A7ZipArchiveEntry> entries = archive.getArchiveEntries();
|
||||
Collections.sort(entries, naturalComparator);
|
||||
|
||||
// Update size and notify changed
|
||||
size = entries.size();
|
||||
notifyDataChanged();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
@@ -217,13 +188,13 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
}
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= entries.size()) {
|
||||
if (index < 0 || index >= size) {
|
||||
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
|
||||
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
|
||||
continue;
|
||||
}
|
||||
|
||||
Pipe pipe = new Pipe(4 * 1024);
|
||||
Pipe pipe = new Pipe(4096 * 16);
|
||||
|
||||
synchronized (streams) {
|
||||
if (streams.get(index) != null) {
|
||||
@@ -233,13 +204,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
streams.notify();
|
||||
}
|
||||
|
||||
try {
|
||||
entries.get(index).extract(pipe.getOutputStream());
|
||||
} catch (ArchiveException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
|
||||
}
|
||||
archiveAccessor.extractTargetIndexToOutputStream(index, pipe.getOutputStream());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,4 +247,4 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,6 @@ class Pipe {
|
||||
public void write(@NonNull byte[] b, int off, int len) throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
while (len != 0) {
|
||||
if (outClosed) {
|
||||
throw new IOException("The OutputStream is closed");
|
||||
}
|
||||
if (inClosed) {
|
||||
throw new IOException("The InputStream is closed");
|
||||
}
|
||||
|
||||
if (head == tail && full) {
|
||||
// The buffer is full, wait for InputStream read bytes
|
||||
try {
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -29,7 +30,6 @@ import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene;
|
||||
import com.hippo.preference.MessagePreference;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.util.ClipboardUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@@ -40,9 +40,9 @@ import okhttp3.HttpUrl;
|
||||
|
||||
public class IdentityCookiePreference extends MessagePreference {
|
||||
|
||||
private String message;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private final SettingsActivity mActivity;
|
||||
private String message;
|
||||
|
||||
public IdentityCookiePreference(Context context) {
|
||||
super(context);
|
||||
@@ -92,7 +92,7 @@ public class IdentityCookiePreference extends MessagePreference {
|
||||
message = EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
|
||||
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
|
||||
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous;
|
||||
setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message)));
|
||||
setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message), Html.FROM_HTML_MODE_LEGACY));
|
||||
message = message.replace("<br>", "\n");
|
||||
} else {
|
||||
setDialogMessage(getContext().getString(R.string.settings_eh_identity_cookies_tourist));
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhProxySelector;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.network.InetValidator;
|
||||
import com.hippo.preference.DialogPreference;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class ProxyPreference extends DialogPreference implements View.OnClickListener {
|
||||
|
||||
private Spinner mType;
|
||||
private TextInputLayout mIpInputLayout;
|
||||
private EditText mIp;
|
||||
private TextInputLayout mPortInputLayout;
|
||||
private EditText mPort;
|
||||
|
||||
public ProxyPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ProxyPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ProxyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setDialogLayoutResource(R.layout.preference_dialog_proxy);
|
||||
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort());
|
||||
}
|
||||
|
||||
private String getProxyTypeText(Context context, int type) {
|
||||
String[] array = context.getResources().getStringArray(R.array.proxy_types);
|
||||
return array[MathUtils.clamp(type, 0, array.length - 1)];
|
||||
}
|
||||
|
||||
private void updateSummary(int type, String ip, int port) {
|
||||
if ((type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS)
|
||||
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))) {
|
||||
type = EhProxySelector.TYPE_SYSTEM;
|
||||
}
|
||||
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
Context context = getContext();
|
||||
setSummary(context.getString(R.string.settings_advanced_proxy_summary_1,
|
||||
getProxyTypeText(context, type),
|
||||
ip,
|
||||
port));
|
||||
} else {
|
||||
Context context = getContext();
|
||||
setSummary(context.getString(R.string.settings_advanced_proxy_summary_2,
|
||||
getProxyTypeText(context, type)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetTextI18n")
|
||||
protected void onDialogCreated(AlertDialog dialog) {
|
||||
super.onDialogCreated(dialog);
|
||||
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this);
|
||||
|
||||
mType = (Spinner) ViewUtils.$$(dialog, R.id.type);
|
||||
mIpInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.ip_input_layout);
|
||||
mIp = (EditText) ViewUtils.$$(dialog, R.id.ip);
|
||||
mPortInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.port_input_layout);
|
||||
mPort = (EditText) ViewUtils.$$(dialog, R.id.port);
|
||||
|
||||
int type = Settings.getProxyType();
|
||||
String[] array = getContext().getResources().getStringArray(R.array.proxy_types);
|
||||
mType.setSelection(MathUtils.clamp(type, 0, array.length));
|
||||
|
||||
mIp.setText(Settings.getProxyIp());
|
||||
|
||||
String portString;
|
||||
int port = Settings.getProxyPort();
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
portString = null;
|
||||
} else {
|
||||
portString = Integer.toString(port);
|
||||
}
|
||||
mPort.setText(portString);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
super.onDialogClosed(positiveResult);
|
||||
mType = null;
|
||||
mIpInputLayout = null;
|
||||
mIp = null;
|
||||
mPortInputLayout = null;
|
||||
mPort = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Dialog dialog = getDialog();
|
||||
Context context = getContext();
|
||||
if (null == dialog || null == context || null == mType ||
|
||||
null == mIpInputLayout || null == mIp ||
|
||||
null == mPortInputLayout || null == mPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
int type = mType.getSelectedItemPosition();
|
||||
|
||||
String ip = mIp.getText().toString().trim();
|
||||
if (ip.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mIpInputLayout.setError(context.getString(R.string.text_is_empty));
|
||||
return;
|
||||
}
|
||||
}
|
||||
mIpInputLayout.setError(null);
|
||||
|
||||
int port;
|
||||
String portString = mPort.getText().toString().trim();
|
||||
if (portString.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mPortInputLayout.setError(context.getString(R.string.text_is_empty));
|
||||
return;
|
||||
} else {
|
||||
port = -1;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
port = Integer.parseInt(portString);
|
||||
} catch (NumberFormatException e) {
|
||||
port = -1;
|
||||
}
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
mPortInputLayout.setError(context.getString(R.string.proxy_invalid_port));
|
||||
return;
|
||||
}
|
||||
}
|
||||
mPortInputLayout.setError(null);
|
||||
|
||||
Settings.putProxyType(type);
|
||||
Settings.putProxyIp(ip);
|
||||
Settings.putProxyPort(port);
|
||||
|
||||
updateSummary(type, ip, port);
|
||||
|
||||
EhApplication.getEhProxySelector(getContext()).updateProxy();
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhProxySelector
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.network.InetValidator
|
||||
import com.hippo.preference.DialogPreference
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class ProxyPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : DialogPreference(context, attrs), View.OnClickListener {
|
||||
private var mType: TextInputLayout? = null
|
||||
private var mIpInputLayout: TextInputLayout? = null
|
||||
private var mIp: EditText? = null
|
||||
private var mPortInputLayout: TextInputLayout? = null
|
||||
private var mPort: EditText? = null
|
||||
private val mArray: Array<String>
|
||||
|
||||
init {
|
||||
mArray = context.resources.getStringArray(R.array.proxy_types)
|
||||
dialogLayoutResource = R.layout.preference_dialog_proxy
|
||||
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort())
|
||||
}
|
||||
|
||||
private fun getProxyTypeText(type: Int): String {
|
||||
return mArray[MathUtils.clamp(type, 0, mArray.size - 1)]
|
||||
}
|
||||
|
||||
private fun updateSummary(type: Int, ip: String?, port: Int) {
|
||||
var type1 = type
|
||||
if ((type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS)
|
||||
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))
|
||||
) {
|
||||
type1 = EhProxySelector.TYPE_SYSTEM
|
||||
}
|
||||
summary = if (type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS) {
|
||||
val context = context
|
||||
context.getString(
|
||||
R.string.settings_advanced_proxy_summary_1,
|
||||
getProxyTypeText(type1),
|
||||
ip,
|
||||
port
|
||||
)
|
||||
} else {
|
||||
val context = context
|
||||
context.getString(
|
||||
R.string.settings_advanced_proxy_summary_2,
|
||||
getProxyTypeText(type1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
super.onDialogCreated(dialog)
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this)
|
||||
mType = ViewUtils.`$$`(dialog, R.id.type) as TextInputLayout
|
||||
val array = context.resources.getStringArray(R.array.proxy_types)
|
||||
mIpInputLayout = ViewUtils.`$$`(dialog, R.id.ip_input_layout) as TextInputLayout
|
||||
mIp = ViewUtils.`$$`(dialog, R.id.ip) as EditText
|
||||
mPortInputLayout = ViewUtils.`$$`(dialog, R.id.port_input_layout) as TextInputLayout
|
||||
mPort = ViewUtils.`$$`(dialog, R.id.port) as EditText
|
||||
val type = Settings.getProxyType()
|
||||
(mType!!.editText as AutoCompleteTextView).setText(
|
||||
array[MathUtils.clamp(
|
||||
type,
|
||||
0,
|
||||
array.size
|
||||
)], false
|
||||
)
|
||||
mIp!!.setText(Settings.getProxyIp())
|
||||
val portString: String?
|
||||
val port = Settings.getProxyPort()
|
||||
portString = if (!InetValidator.isValidInetPort(port)) {
|
||||
null
|
||||
} else {
|
||||
port.toString()
|
||||
}
|
||||
mPort!!.setText(portString)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
super.onDialogClosed(positiveResult)
|
||||
mType = null
|
||||
mIpInputLayout = null
|
||||
mIp = null
|
||||
mPortInputLayout = null
|
||||
mPort = null
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val dialog = dialog
|
||||
val context: Context = context
|
||||
if (null == dialog || null == mType || null == mIpInputLayout || null == mIp || null == mPortInputLayout || null == mPort) {
|
||||
return
|
||||
}
|
||||
val type = mArray.indexOf(mType!!.editText!!.text.toString())
|
||||
val ip = mIp!!.text.toString().trim { it <= ' ' }
|
||||
if (ip.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mIpInputLayout!!.error = context.getString(R.string.text_is_empty)
|
||||
return
|
||||
}
|
||||
}
|
||||
mIpInputLayout!!.error = null
|
||||
val port: Int
|
||||
val portString = mPort!!.text.toString().trim { it <= ' ' }
|
||||
if (portString.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mPortInputLayout!!.error = context.getString(R.string.text_is_empty)
|
||||
return
|
||||
} else {
|
||||
port = -1
|
||||
}
|
||||
} else {
|
||||
port = try {
|
||||
portString.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
-1
|
||||
}
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
mPortInputLayout!!.error = context.getString(R.string.proxy_invalid_port)
|
||||
return
|
||||
}
|
||||
}
|
||||
mPortInputLayout!!.error = null
|
||||
Settings.putProxyType(type)
|
||||
Settings.putProxyIp(ip)
|
||||
Settings.putProxyPort(port)
|
||||
updateSummary(type, ip, port)
|
||||
EhApplication.getEhProxySelector(getContext()).updateProxy()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
public class VersionPreference extends Preference {
|
||||
|
||||
public VersionPreference(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public VersionPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public VersionPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
setTitle(R.string.settings_about_version);
|
||||
|
||||
String version;
|
||||
try {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
version = pInfo.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
version = context.getString(R.string.error_unknown);
|
||||
}
|
||||
setSummary(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.hippo.Native
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.preference.MessagePreference
|
||||
|
||||
class VersionPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : MessagePreference(context, attrs) {
|
||||
init {
|
||||
setTitle(R.string.settings_about_version)
|
||||
@Suppress("DEPRECATION") val version: String = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
context.getString(R.string.error_unknown)
|
||||
}
|
||||
summary = version
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setTitle(R.string.show_library_version)
|
||||
builder.setMessage(Native.getlibarchiveVersion() + "\nlzma(xz-utils) " + Native.getliblzmaVersion() + "\nzlib " + Native.getzlibVersion() + "\nlibjpeg-turbo " + Native.getlibjpeg_turboVersion())
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,7 @@ public final class SpiderQueen implements Runnable {
|
||||
private final int mWorkerMaxCount;
|
||||
private final int mPreloadNumber;
|
||||
private final int mDownloadDelay;
|
||||
private final AtomicReference<String> showKey = new AtomicReference<>();
|
||||
private int mReadReference = 0;
|
||||
private int mDownloadReference = 0;
|
||||
// It mQueenThread is null, failed or stopped
|
||||
@@ -153,7 +154,6 @@ public final class SpiderQueen implements Runnable {
|
||||
private volatile int[] mPageStateArray;
|
||||
// For download, when it go to mPageStateArray.size(), done
|
||||
private volatile int mDownloadPage = -1;
|
||||
private final AtomicReference<String> showKey = new AtomicReference<>();
|
||||
|
||||
private SpiderQueen(EhApplication application, @NonNull GalleryInfo galleryInfo) {
|
||||
mHttpClient = EhApplication.getOkHttpClient(application);
|
||||
|
||||
@@ -26,18 +26,19 @@ import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.material.app.MaterialActivity;
|
||||
import rikka.insets.WindowInsetsHelper;
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory;
|
||||
|
||||
public abstract class EhActivity extends MaterialActivity {
|
||||
public abstract class EhActivity extends AppCompatActivity {
|
||||
|
||||
private static final String THEME_DEFAULT = "DEFAULT";
|
||||
private static final String THEME_BLACK = "BLACK";
|
||||
@@ -47,10 +48,8 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
}
|
||||
|
||||
public static String getTheme(Context context) {
|
||||
if (isBlackNightTheme()
|
||||
&& isNightMode(context.getResources().getConfiguration()))
|
||||
if (isBlackNightTheme() && isNightMode(context.getResources().getConfiguration()))
|
||||
return THEME_BLACK;
|
||||
|
||||
return THEME_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -58,38 +57,67 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyUserThemeResource(@NonNull Resources.Theme theme, boolean isDecorView) {
|
||||
theme.applyStyle(getThemeStyleRes(this), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String computeUserThemeKey() {
|
||||
return getTheme(this);
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
public int getThemeStyleRes(Context context) {
|
||||
switch (getTheme(context)) {
|
||||
case THEME_BLACK:
|
||||
return R.style.ThemeOverlay_Black;
|
||||
case THEME_DEFAULT:
|
||||
default:
|
||||
return R.style.ThemeOverlay;
|
||||
if (THEME_BLACK.equals(getTheme(context))) {
|
||||
return R.style.ThemeOverlay_Black;
|
||||
}
|
||||
return R.style.ThemeOverlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
|
||||
if (getParent() == null) {
|
||||
theme.applyStyle(resid, true);
|
||||
} else {
|
||||
try {
|
||||
theme.setTo(getParent().getTheme());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
theme.applyStyle(resid, false);
|
||||
}
|
||||
theme.applyStyle(getThemeStyleRes(this), true);
|
||||
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNightModeChanged(int mode) {
|
||||
getTheme().applyStyle(getThemeStyleRes(this), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
getLayoutInflater().setFactory2((new LayoutInflaterFactory(getDelegate())).addOnViewCreatedListener(WindowInsetsHelper.getLISTENER()));
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
|
||||
Window window = getWindow();
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
window.getDecorView().post(() -> {
|
||||
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
|
||||
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.setNavigationBarDividerColor(getColor(R.color.navigation_bar_divider));
|
||||
}
|
||||
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(false);
|
||||
}
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
((EhApplication) getApplication()).registerActivity(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
((EhApplication) getApplication()).unregisterActivity(this);
|
||||
}
|
||||
|
||||
@@ -103,26 +131,4 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTranslucentSystemBars() {
|
||||
super.onApplyTranslucentSystemBars();
|
||||
Window window = getWindow();
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
window.getDecorView().post(() -> {
|
||||
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
|
||||
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
|
||||
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(false);
|
||||
}
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ import android.webkit.MimeTypeMap;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -58,6 +57,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -65,6 +65,8 @@ import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.BuildConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -104,7 +106,6 @@ import java.io.OutputStream;
|
||||
|
||||
import rikka.core.res.ConfigurationKt;
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener,
|
||||
GalleryView.Listener {
|
||||
@@ -129,6 +130,36 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
private GalleryInfo mGalleryInfo;
|
||||
private int mPage;
|
||||
private String mCacheFileName;
|
||||
ActivityResultLauncher<String> saveImageToLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.CreateDocument(),
|
||||
uri -> {
|
||||
if (uri != null) {
|
||||
String filepath = AppConfig.getExternalTempDir() + File.separator + mCacheFileName;
|
||||
File cachefile = new File(filepath);
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = new FileInputStream(cachefile);
|
||||
os = resolver.openOutputStream(uri);
|
||||
IOUtils.copy(is, os);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
runOnUiThread(() -> Toast.makeText(GalleryActivity.this, getString(R.string.image_saved, uri.getPath()), Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cachefile.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@Nullable
|
||||
private GLRootView mGLRootView;
|
||||
@Nullable
|
||||
@@ -190,50 +221,8 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
private int mLayoutMode;
|
||||
private int mSize;
|
||||
private int mCurrentIndex;
|
||||
|
||||
private int mSavingPage = -1;
|
||||
|
||||
ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
result -> {
|
||||
if (result && mSavingPage != -1) {
|
||||
saveImage(mSavingPage);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mSavingPage = -1;
|
||||
});
|
||||
ActivityResultLauncher<String> saveImageToLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.CreateDocument(),
|
||||
uri -> {
|
||||
if (uri != null) {
|
||||
String filepath = AppConfig.getExternalTempDir() + File.separator + mCacheFileName;
|
||||
File cachefile = new File(filepath);
|
||||
|
||||
ContentResolver resolver = getContentResolver();
|
||||
|
||||
IoThreadPoolExecutor.getInstance().execute(() -> {
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = new FileInputStream(cachefile);
|
||||
os = resolver.openOutputStream(uri);
|
||||
IOUtils.copy(is, os);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
runOnUiThread(() -> Toast.makeText(GalleryActivity.this, getString(R.string.image_saved, uri.getPath()), Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cachefile.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
private void buildProvider() {
|
||||
if (mGalleryProvider != null) {
|
||||
return;
|
||||
@@ -249,13 +238,9 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
} else if (Intent.ACTION_VIEW.equals(mAction)) {
|
||||
if (mUri != null) {
|
||||
// Only support zip now
|
||||
try {
|
||||
grantUriPermission(BuildConfig.APPLICATION_ID, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} catch (Exception e) {
|
||||
// Some stupid file manager send us a uri and don't allow us to read it
|
||||
// java.lang.SecurityException: UID 10671 does not have permission to
|
||||
// content://com.UCMobile.fileProvider/external_files/BaiduNetdisk/getvoice [user 0]
|
||||
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mGalleryProvider = new ArchiveGalleryProvider(this, mUri);
|
||||
@@ -304,20 +289,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
switch (Settings.getReadTheme()) {
|
||||
case 1:
|
||||
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_YES, false);
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
break;
|
||||
case 2:
|
||||
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_NO, false);
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
break;
|
||||
}
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean respectDefaultNightMode() {
|
||||
return Settings.getReadTheme() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"WrongConstant"})
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -359,7 +339,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
.setStartPosition(Settings.getStartPosition())
|
||||
.setStartPage(startPage)
|
||||
.setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground))
|
||||
.setEdgeColor(ResourcesKt.resolveColor(getTheme(), R.attr.colorEdgeEffect) & 0xffffff | 0x33000000)
|
||||
.setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0)
|
||||
.setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0)
|
||||
.setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height))
|
||||
@@ -485,7 +464,16 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main);
|
||||
mainLayout.addView(new GalleryGuideView(this));
|
||||
}
|
||||
}
|
||||
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
result -> {
|
||||
if (result && mSavingPage != -1) {
|
||||
saveImage(mSavingPage);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mSavingPage = -1;
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
@@ -981,7 +969,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
|
||||
private void showPageDialog(final int page) {
|
||||
Resources resources = GalleryActivity.this.getResources();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(GalleryActivity.this);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this);
|
||||
builder.setTitle(resources.getString(R.string.page_menu_title, page + 1));
|
||||
|
||||
final CharSequence[] items;
|
||||
@@ -1022,6 +1010,29 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getGalleryDetailUrl() {
|
||||
long gid;
|
||||
String token;
|
||||
if (mGalleryInfo != null) {
|
||||
gid = mGalleryInfo.gid;
|
||||
token = mGalleryInfo.token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return EhUrl.getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent outContent) {
|
||||
super.onProvideAssistContent(outContent);
|
||||
|
||||
String url = getGalleryDetailUrl();
|
||||
if (url != null) {
|
||||
outContent.setWebUri(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
private class GalleryMenuHelper implements DialogInterface.OnClickListener {
|
||||
|
||||
private final View mView;
|
||||
@@ -1030,15 +1041,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
private final Spinner mScaleMode;
|
||||
private final Spinner mStartPosition;
|
||||
private final Spinner mReadTheme;
|
||||
private final Switch mKeepScreenOn;
|
||||
private final Switch mShowClock;
|
||||
private final Switch mShowProgress;
|
||||
private final Switch mShowBattery;
|
||||
private final Switch mShowPageInterval;
|
||||
private final Switch mVolumePage;
|
||||
private final Switch mReverseVolumePage;
|
||||
private final Switch mReadingFullscreen;
|
||||
private final Switch mCustomScreenLightness;
|
||||
private final MaterialSwitch mKeepScreenOn;
|
||||
private final MaterialSwitch mShowClock;
|
||||
private final MaterialSwitch mShowProgress;
|
||||
private final MaterialSwitch mShowBattery;
|
||||
private final MaterialSwitch mShowPageInterval;
|
||||
private final MaterialSwitch mVolumePage;
|
||||
private final MaterialSwitch mReverseVolumePage;
|
||||
private final MaterialSwitch mReadingFullscreen;
|
||||
private final MaterialSwitch mCustomScreenLightness;
|
||||
private final SeekBar mScreenLightness;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -1194,7 +1205,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
|
||||
private void onTapMenuArea() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(GalleryActivity.this);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this);
|
||||
GalleryMenuHelper helper = new GalleryMenuHelper(builder.getContext());
|
||||
builder.setTitle(R.string.gallery_menu_title)
|
||||
.setView(helper.getView())
|
||||
@@ -1282,26 +1293,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getGalleryDetailUrl() {
|
||||
long gid;
|
||||
String token;
|
||||
if (mGalleryInfo != null) {
|
||||
gid = mGalleryInfo.gid;
|
||||
token = mGalleryInfo.token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return EhUrl.getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent outContent) {
|
||||
super.onProvideAssistContent(outContent);
|
||||
|
||||
String url = getGalleryDetailUrl();
|
||||
if (url != null) {
|
||||
outContent.setWebUri(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,18 +22,15 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.verify.domain.DomainVerificationManager;
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
@@ -43,14 +40,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.app.EditTextDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
@@ -59,7 +56,6 @@ import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||
import com.hippo.ehviewer.ui.scene.AnalyticsScene;
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene;
|
||||
import com.hippo.ehviewer.ui.scene.CookieSignInScene;
|
||||
import com.hippo.ehviewer.ui.scene.DownloadsScene;
|
||||
@@ -86,7 +82,6 @@ import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.BitmapUtils;
|
||||
import com.hippo.util.ClipboardUtil;
|
||||
import com.hippo.widget.DrawerView;
|
||||
import com.hippo.widget.LoadImageView;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
@@ -97,8 +92,6 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public final class MainActivity extends StageActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
@@ -107,7 +100,6 @@ public final class MainActivity extends StageActivity
|
||||
static {
|
||||
registerLaunchMode(SecurityScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
registerLaunchMode(WarningScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
registerLaunchMode(AnalyticsScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
registerLaunchMode(SignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
registerLaunchMode(WebViewSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
registerLaunchMode(CookieSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
|
||||
@@ -141,10 +133,6 @@ public final class MainActivity extends StageActivity
|
||||
private NavigationView mNavView;
|
||||
@Nullable
|
||||
private DrawerView mRightDrawer;
|
||||
@Nullable
|
||||
private LoadImageView mAvatar;
|
||||
@Nullable
|
||||
private TextView mDisplayName;
|
||||
private int mNavCheckedItem = 0;
|
||||
|
||||
@Override
|
||||
@@ -155,12 +143,10 @@ public final class MainActivity extends StageActivity
|
||||
@NonNull
|
||||
@Override
|
||||
protected Announcer getLaunchAnnouncer() {
|
||||
if (!TextUtils.isEmpty(Settings.getSecurity())) {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
|
||||
return new Announcer(SecurityScene.class);
|
||||
} else if (Settings.getShowWarning()) {
|
||||
return new Announcer(WarningScene.class);
|
||||
} else if (Settings.getAskAnalytics()) {
|
||||
return new Announcer(AnalyticsScene.class);
|
||||
} else if (EhUtils.needSignedIn(this)) {
|
||||
return new Announcer(SignInScene.class);
|
||||
} else if (Settings.getSelectSite()) {
|
||||
@@ -175,7 +161,7 @@ public final class MainActivity extends StageActivity
|
||||
// Sometimes scene can't show directly
|
||||
private Announcer processAnnouncer(Announcer announcer) {
|
||||
if (0 == getSceneCount()) {
|
||||
if (!TextUtils.isEmpty(Settings.getSecurity())) {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
|
||||
Bundle newArgs = new Bundle();
|
||||
newArgs.putString(SecurityScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
|
||||
newArgs.putBundle(SecurityScene.KEY_TARGET_ARGS, announcer.getArgs());
|
||||
@@ -185,11 +171,6 @@ public final class MainActivity extends StageActivity
|
||||
newArgs.putString(WarningScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
|
||||
newArgs.putBundle(WarningScene.KEY_TARGET_ARGS, announcer.getArgs());
|
||||
return new Announcer(WarningScene.class).setArgs(newArgs);
|
||||
} else if (Settings.getAskAnalytics()) {
|
||||
Bundle newArgs = new Bundle();
|
||||
newArgs.putString(AnalyticsScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
|
||||
newArgs.putBundle(AnalyticsScene.KEY_TARGET_ARGS, announcer.getArgs());
|
||||
return new Announcer(AnalyticsScene.class).setArgs(newArgs);
|
||||
} else if (EhUtils.needSignedIn(this)) {
|
||||
Bundle newArgs = new Bundle();
|
||||
newArgs.putString(SignInScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
|
||||
@@ -329,19 +310,6 @@ public final class MainActivity extends StageActivity
|
||||
mDrawerLayout.setStatusBarBackgroundColor(0);
|
||||
mNavView = (NavigationView) ViewUtils.$$(this, R.id.nav_view);
|
||||
mRightDrawer = (DrawerView) ViewUtils.$$(this, R.id.right_drawer);
|
||||
View headerLayout = mNavView.getHeaderView(0);
|
||||
mAvatar = (LoadImageView) ViewUtils.$$(headerLayout, R.id.avatar);
|
||||
mDisplayName = (TextView) ViewUtils.$$(headerLayout, R.id.display_name);
|
||||
ViewUtils.$$(headerLayout, R.id.night_mode).setOnClickListener(v -> {
|
||||
int theme = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) > 0 ? AppCompatDelegate.MODE_NIGHT_NO : AppCompatDelegate.MODE_NIGHT_YES;
|
||||
DayNightDelegate.setDefaultNightMode(theme);
|
||||
recreate();
|
||||
if (Settings.getTheme() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
|
||||
Settings.putTheme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
updateProfile();
|
||||
|
||||
if (mNavView != null) {
|
||||
mNavView.setNavigationItemSelectedListener(this);
|
||||
@@ -389,13 +357,19 @@ public final class MainActivity extends StageActivity
|
||||
break;
|
||||
}
|
||||
if (hasUnverified) {
|
||||
new AlertDialog.Builder(this)
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.app_link_not_verified_title)
|
||||
.setMessage(R.string.app_link_not_verified_message)
|
||||
.setPositiveButton(R.string.open_settings, (dialogInterface, i) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
try {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
} catch (Throwable t) {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(R.string.dont_show_again, (dialogInterface, i) -> Settings.putAppLinkVerifyTip(true))
|
||||
@@ -409,7 +383,7 @@ public final class MainActivity extends StageActivity
|
||||
if (null == uniFile || uniFile.ensureDir()) {
|
||||
return;
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.waring)
|
||||
.setMessage(R.string.invalid_download_location)
|
||||
.setPositiveButton(R.string.get_it, null)
|
||||
@@ -449,23 +423,21 @@ public final class MainActivity extends StageActivity
|
||||
mDrawerLayout = null;
|
||||
mNavView = null;
|
||||
mRightDrawer = null;
|
||||
mAvatar = null;
|
||||
mDisplayName = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this) && EhApplication.locked) {
|
||||
startScene(new Announcer(SecurityScene.class), true);
|
||||
}
|
||||
super.onResume();
|
||||
|
||||
setNavCheckedItem(mNavCheckedItem);
|
||||
|
||||
checkClipboardUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransactScene() {
|
||||
super.onTransactScene();
|
||||
|
||||
checkClipboardUrl();
|
||||
}
|
||||
|
||||
@@ -555,25 +527,6 @@ public final class MainActivity extends StageActivity
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProfile() {
|
||||
if (null == mAvatar || null == mDisplayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
String avatarUrl = Settings.getAvatar();
|
||||
if (TextUtils.isEmpty(avatarUrl)) {
|
||||
mAvatar.load(R.drawable.default_avatar);
|
||||
} else {
|
||||
mAvatar.load(avatarUrl, avatarUrl);
|
||||
}
|
||||
|
||||
String displayName = Settings.getDisplayName();
|
||||
if (TextUtils.isEmpty(displayName)) {
|
||||
displayName = getString(R.string.default_display_name);
|
||||
}
|
||||
mDisplayName.setText(displayName);
|
||||
}
|
||||
|
||||
public void addAboveSnackView(View view) {
|
||||
if (mStageLayout != null) {
|
||||
mStageLayout.addAboveSnackView(view);
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -41,6 +42,7 @@ public final class SettingsActivity extends EhActivity {
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
|
||||
.replace(R.id.fragment, new SettingsFragment())
|
||||
.commitAllowingStateLoss();
|
||||
}
|
||||
|
||||
@@ -17,49 +17,23 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.Analytics;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.util.AppHelper;
|
||||
import com.microsoft.appcenter.distribute.Distribute;
|
||||
|
||||
public class AboutFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final String KEY_AUTHOR = "author";
|
||||
//private static final String KEY_DONATE = "donate";
|
||||
private static final String KEY_CHECK_FOR_UPDATES = "check_for_updates";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.about_settings);
|
||||
|
||||
Preference author = findPreference(KEY_AUTHOR);
|
||||
//Preference donate = findPreference(KEY_DONATE);
|
||||
Preference checkForUpdate = findPreference(KEY_CHECK_FOR_UPDATES);
|
||||
|
||||
author.setSummary(Html.fromHtml(getString(R.string.settings_about_author_summary).replace('$', '@')));
|
||||
|
||||
author.setOnPreferenceClickListener(this);
|
||||
//donate.setOnPreferenceClickListener(this);
|
||||
checkForUpdate.setOnPreferenceClickListener(this);
|
||||
checkForUpdate.setVisible(Analytics.isEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
if (KEY_AUTHOR.equals(key)) {
|
||||
AppHelper.sendEmail(requireActivity(), EhApplication.getDeveloperEmail(),
|
||||
"About EhViewer", null);
|
||||
} else if (KEY_CHECK_FOR_UPDATES.equals(key)) {
|
||||
Distribute.checkForUpdate();
|
||||
}
|
||||
return true;
|
||||
author.setSummary(Html.fromHtml(getString(R.string.settings_about_author_summary).replace('$', '@'), Html.FROM_HTML_MODE_LEGACY));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -27,8 +28,11 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.BuildConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
@@ -49,12 +53,9 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import rikka.material.app.LocaleDelegate;
|
||||
|
||||
public class AdvancedFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final String KEY_DUMP_LOGCAT = "dump_logcat";
|
||||
@@ -76,7 +77,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
AlertDialog alertDialog = new AlertDialog.Builder(requireActivity())
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show();
|
||||
@@ -193,7 +194,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
AlertDialog alertDialog = new AlertDialog.Builder(requireActivity())
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show();
|
||||
@@ -221,7 +222,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.advanced_settings);
|
||||
|
||||
Preference dumpLogcat = findPreference(KEY_DUMP_LOGCAT);
|
||||
@@ -277,9 +278,15 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
}
|
||||
return true;
|
||||
} else if (KEY_OPEN_BY_DEFAULT.equals(key)) {
|
||||
Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + requireContext().getPackageName()));
|
||||
startActivity(intent);
|
||||
try {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + requireContext().getPackageName()));
|
||||
startActivity(intent);
|
||||
} catch (Throwable t) {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.parse("package:" + requireContext().getPackageName()));
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -290,9 +297,9 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
String key = preference.getKey();
|
||||
if (KEY_APP_LANGUAGE.equals(key)) {
|
||||
if ("system".equals(newValue)) {
|
||||
LocaleDelegate.setDefaultLocale(LocaleDelegate.getSystemLocale());
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
|
||||
} else {
|
||||
LocaleDelegate.setDefaultLocale(Locale.forLanguageTag((String) newValue));
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags((String) newValue));
|
||||
}
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
@@ -38,10 +55,26 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
try {
|
||||
Fragment classObj = (Fragment) Class.forName(key).newInstance();
|
||||
requireActivity().getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.replace(R.id.fragment, classObj)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | java.lang.InstantiationException ignored) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setTitle(@StringRes int string) {
|
||||
requireActivity().setTitle(string);
|
||||
}
|
||||
@@ -53,11 +86,4 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
public void showTip(CharSequence message, int length) {
|
||||
((SettingsActivity) requireActivity()).showTip(message, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
RecyclerViewKt.fixEdgeEffect(recyclerView, false, true);
|
||||
return recyclerView;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import android.os.Bundle;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
@@ -59,7 +59,7 @@ public class DownloadFragment extends BasePreferenceFragment {
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.download_settings);
|
||||
|
||||
Preference mediaScan = findPreference(Settings.KEY_MEDIA_SCAN);
|
||||
@@ -99,7 +99,7 @@ public class DownloadFragment extends BasePreferenceFragment {
|
||||
if (KEY_DOWNLOAD_LOCATION.equals(key)) {
|
||||
UniFile file = Settings.getDownloadLocation();
|
||||
if (file != null && !UniFile.isFileUri(Settings.getDownloadLocation().getUri())) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.settings_download_download_location)
|
||||
.setMessage(file.getUri().toString())
|
||||
.setPositiveButton(R.string.pick_new_download_location, (dialogInterface, i) -> openDirPickerL())
|
||||
|
||||
@@ -21,18 +21,17 @@ import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public class EhFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.eh_settings);
|
||||
|
||||
Preference theme = findPreference(Settings.KEY_THEME);
|
||||
@@ -62,7 +61,7 @@ public class EhFragment extends BasePreferenceFragment {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
String key = preference.getKey();
|
||||
if (Settings.KEY_THEME.equals(key)) {
|
||||
DayNightDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
|
||||
AppCompatDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
} else if (Settings.KEY_GALLERY_SITE.equals(key)) {
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.checkbox.MaterialCheckBox;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView;
|
||||
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.EhFilter;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
import com.hippo.view.ViewTransition;
|
||||
import com.hippo.yorozuya.LayoutUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
|
||||
public class FilterFragment extends BaseFragment {
|
||||
|
||||
@Nullable
|
||||
private ViewTransition mViewTransition;
|
||||
@Nullable
|
||||
private FilterAdapter mAdapter;
|
||||
@Nullable
|
||||
private FilterList mFilterList;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.activity_filter, container, false);
|
||||
|
||||
mFilterList = new FilterList();
|
||||
|
||||
RecyclerView recyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
|
||||
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
|
||||
mViewTransition = new ViewTransition(recyclerView, tip);
|
||||
FloatingActionButton fab = view.findViewById(R.id.fab);
|
||||
|
||||
Drawable drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter);
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
tip.setCompoundDrawables(null, drawable, null, null);
|
||||
|
||||
mAdapter = new FilterAdapter();
|
||||
mAdapter.setHasStableIds(true);
|
||||
recyclerView.setAdapter(mAdapter);
|
||||
recyclerView.setClipToPadding(false);
|
||||
recyclerView.setClipChildren(false);
|
||||
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
|
||||
LinearDividerItemDecoration.VERTICAL,
|
||||
ResourcesKt.resolveColor(requireActivity().getTheme(), R.attr.dividerColor),
|
||||
LayoutUtils.dp2pix(requireActivity(), 1));
|
||||
decoration.setShowLastDivider(true);
|
||||
recyclerView.addItemDecoration(decoration);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
recyclerView.setHasFixedSize(true);
|
||||
DefaultItemAnimator defaultItemAnimator = (DefaultItemAnimator) recyclerView.getItemAnimator();
|
||||
if (defaultItemAnimator != null) {
|
||||
defaultItemAnimator.setSupportsChangeAnimations(false);
|
||||
}
|
||||
|
||||
fab.setOnClickListener(v -> showAddFilterDialog());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
updateView(false);
|
||||
}
|
||||
|
||||
private void updateView(boolean animation) {
|
||||
if (null == mViewTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == mFilterList || 0 == mFilterList.size()) {
|
||||
mViewTransition.showView(1, animation);
|
||||
} else {
|
||||
mViewTransition.showView(0, animation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
mViewTransition = null;
|
||||
mAdapter = null;
|
||||
mFilterList = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.activity_filter, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_tip) {
|
||||
showTipDialog();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void showTipDialog() {
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.filter)
|
||||
.setMessage(R.string.filter_tip)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showAddFilterDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.add_filter)
|
||||
.setView(R.layout.dialog_add_filter)
|
||||
.setPositiveButton(R.string.add, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
AddFilterDialogHelper helper = new AddFilterDialogHelper();
|
||||
helper.setDialog(dialog);
|
||||
}
|
||||
|
||||
private void showDeleteFilterDialog(final Filter filter) {
|
||||
String message = getString(R.string.delete_filter, filter.text);
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
if (DialogInterface.BUTTON_POSITIVE != which || null == mFilterList) {
|
||||
return;
|
||||
}
|
||||
mFilterList.delete(filter);
|
||||
if (null != mAdapter) {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateView(true);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private class AddFilterDialogHelper implements View.OnClickListener {
|
||||
|
||||
@Nullable
|
||||
private AlertDialog mDialog;
|
||||
@Nullable
|
||||
private Spinner mSpinner;
|
||||
@Nullable
|
||||
private TextInputLayout mInputLayout;
|
||||
@Nullable
|
||||
private EditText mEditText;
|
||||
|
||||
public void setDialog(AlertDialog dialog) {
|
||||
mDialog = dialog;
|
||||
mSpinner = (Spinner) ViewUtils.$$(dialog, R.id.spinner);
|
||||
mInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.text_input_layout);
|
||||
mEditText = mInputLayout.getEditText();
|
||||
View button = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
if (null != button) {
|
||||
button.setOnClickListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (null == mFilterList || null == mDialog || null == mSpinner ||
|
||||
null == mInputLayout || null == mEditText) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = mEditText.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
mInputLayout.setError(getString(R.string.text_is_empty));
|
||||
return;
|
||||
} else {
|
||||
mInputLayout.setError(null);
|
||||
}
|
||||
int mode = mSpinner.getSelectedItemPosition();
|
||||
|
||||
Filter filter = new Filter();
|
||||
filter.mode = mode;
|
||||
filter.text = text;
|
||||
if (!mFilterList.add(filter)) {
|
||||
mInputLayout.setError(getString(R.string.label_text_exist));
|
||||
return;
|
||||
} else {
|
||||
mInputLayout.setError(null);
|
||||
}
|
||||
|
||||
if (null != mAdapter) {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateView(true);
|
||||
|
||||
mDialog.dismiss();
|
||||
mDialog = null;
|
||||
mSpinner = null;
|
||||
mInputLayout = null;
|
||||
mEditText = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FilterHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final MaterialCheckBox checkbox;
|
||||
private final TextView text;
|
||||
private final ImageView delete;
|
||||
|
||||
public FilterHolder(View itemView) {
|
||||
super(itemView);
|
||||
checkbox = itemView.findViewById(R.id.checkbox);
|
||||
text = itemView.findViewById(R.id.text);
|
||||
delete = itemView.findViewById(R.id.delete);
|
||||
}
|
||||
}
|
||||
|
||||
private class FilterAdapter extends RecyclerView.Adapter<FilterHolder> {
|
||||
|
||||
private static final int TYPE_ITEM = 0;
|
||||
private static final int TYPE_HEADER = 1;
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (null == mFilterList) {
|
||||
return TYPE_ITEM;
|
||||
}
|
||||
|
||||
if (mFilterList.get(position).mode == FilterList.MODE_HEADER) {
|
||||
return TYPE_HEADER;
|
||||
} else {
|
||||
return TYPE_ITEM;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FilterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
int layoutId;
|
||||
switch (viewType) {
|
||||
default:
|
||||
case TYPE_ITEM:
|
||||
layoutId = R.layout.item_filter;
|
||||
break;
|
||||
case TYPE_HEADER:
|
||||
layoutId = R.layout.item_filter_header;
|
||||
break;
|
||||
}
|
||||
|
||||
return new FilterHolder(getLayoutInflater().inflate(layoutId, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FilterHolder holder, int position) {
|
||||
if (null == mFilterList) {
|
||||
return;
|
||||
}
|
||||
|
||||
Filter filter = mFilterList.get(position);
|
||||
if (FilterList.MODE_HEADER == filter.mode) {
|
||||
holder.text.setText(filter.text);
|
||||
} else {
|
||||
holder.checkbox.setText(filter.text);
|
||||
holder.checkbox.setChecked(filter.enable);
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
mFilterList.trigger(filter);
|
||||
|
||||
//for updating delete line on filter text
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
holder.delete.setOnClickListener(v -> showDeleteFilterDialog(filter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return null != mFilterList ? mFilterList.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (mFilterList == null) {
|
||||
return 0;
|
||||
} else {
|
||||
Filter filter = mFilterList.get(position);
|
||||
if (filter.getId() != null) {
|
||||
return (filter.text.hashCode() >> filter.mode) + filter.getId();
|
||||
}
|
||||
return filter.text.hashCode() >> filter.mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FilterList {
|
||||
|
||||
public static final int MODE_HEADER = -1;
|
||||
|
||||
private final EhFilter mEhFilter;
|
||||
private final List<Filter> mTitleFilterList;
|
||||
private final List<Filter> mUploaderFilterList;
|
||||
private final List<Filter> mTagFilterList;
|
||||
private final List<Filter> mTagNamespaceFilterList;
|
||||
|
||||
private Filter mTitleHeader;
|
||||
private Filter mUploaderHeader;
|
||||
private Filter mTagHeader;
|
||||
private Filter mTagNamespaceHeader;
|
||||
|
||||
public FilterList() {
|
||||
mEhFilter = EhFilter.getInstance();
|
||||
mTitleFilterList = mEhFilter.getTitleFilterList();
|
||||
mUploaderFilterList = mEhFilter.getUploaderFilterList();
|
||||
mTagFilterList = mEhFilter.getTagFilterList();
|
||||
mTagNamespaceFilterList = mEhFilter.getTagNamespaceFilterList();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
int count = 0;
|
||||
int size = mTitleFilterList.size();
|
||||
count += 0 == size ? 0 : size + 1;
|
||||
size = mUploaderFilterList.size();
|
||||
count += 0 == size ? 0 : size + 1;
|
||||
size = mTagFilterList.size();
|
||||
count += 0 == size ? 0 : size + 1;
|
||||
size = mTagNamespaceFilterList.size();
|
||||
count += 0 == size ? 0 : size + 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
private Filter getTitleHeader() {
|
||||
if (null == mTitleHeader) {
|
||||
mTitleHeader = new Filter();
|
||||
mTitleHeader.mode = MODE_HEADER;
|
||||
mTitleHeader.text = getString(R.string.filter_title);
|
||||
}
|
||||
return mTitleHeader;
|
||||
}
|
||||
|
||||
private Filter getUploaderHeader() {
|
||||
if (null == mUploaderHeader) {
|
||||
mUploaderHeader = new Filter();
|
||||
mUploaderHeader.mode = MODE_HEADER;
|
||||
mUploaderHeader.text = getString(R.string.filter_uploader);
|
||||
}
|
||||
return mUploaderHeader;
|
||||
}
|
||||
|
||||
private Filter getTagHeader() {
|
||||
if (null == mTagHeader) {
|
||||
mTagHeader = new Filter();
|
||||
mTagHeader.mode = MODE_HEADER;
|
||||
mTagHeader.text = getString(R.string.filter_tag);
|
||||
}
|
||||
return mTagHeader;
|
||||
}
|
||||
|
||||
private Filter getTagNamespaceHeader() {
|
||||
if (null == mTagNamespaceHeader) {
|
||||
mTagNamespaceHeader = new Filter();
|
||||
mTagNamespaceHeader.mode = MODE_HEADER;
|
||||
mTagNamespaceHeader.text = getString(R.string.filter_tag_namespace);
|
||||
}
|
||||
return mTagNamespaceHeader;
|
||||
}
|
||||
|
||||
public Filter get(int index) {
|
||||
int size = mTitleFilterList.size();
|
||||
if (0 != size) {
|
||||
if (index == 0) {
|
||||
return getTitleHeader();
|
||||
} else if (index <= size) {
|
||||
return mTitleFilterList.get(index - 1);
|
||||
} else {
|
||||
index -= size + 1;
|
||||
}
|
||||
}
|
||||
|
||||
size = mUploaderFilterList.size();
|
||||
if (0 != size) {
|
||||
if (index == 0) {
|
||||
return getUploaderHeader();
|
||||
} else if (index <= size) {
|
||||
return mUploaderFilterList.get(index - 1);
|
||||
} else {
|
||||
index -= size + 1;
|
||||
}
|
||||
}
|
||||
|
||||
size = mTagFilterList.size();
|
||||
if (0 != size) {
|
||||
if (index == 0) {
|
||||
return getTagHeader();
|
||||
} else if (index <= size) {
|
||||
return mTagFilterList.get(index - 1);
|
||||
} else {
|
||||
index -= size + 1;
|
||||
}
|
||||
}
|
||||
|
||||
size = mTagNamespaceFilterList.size();
|
||||
if (0 != size) {
|
||||
if (index == 0) {
|
||||
return getTagNamespaceHeader();
|
||||
} else if (index <= size) {
|
||||
return mTagNamespaceFilterList.get(index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
public boolean add(Filter filter) {
|
||||
return mEhFilter.addFilter(filter);
|
||||
}
|
||||
|
||||
public void delete(Filter filter) {
|
||||
mEhFilter.deleteFilter(filter);
|
||||
}
|
||||
|
||||
public void trigger(Filter filter) {
|
||||
mEhFilter.triggerFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.filter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhFilter
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.view.ViewTransition
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class FilterFragment : BaseFragment() {
|
||||
private var mViewTransition: ViewTransition? = null
|
||||
private var mAdapter: FilterAdapter = FilterAdapter()
|
||||
private var mFilterList: FilterList = FilterList()
|
||||
private val mMenuProvider: MenuProvider = FilterMenuProvider()
|
||||
|
||||
inner class FilterMenuProvider : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.activity_filter, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
val itemId = menuItem.itemId
|
||||
if (itemId == R.id.action_tip) {
|
||||
showTipDialog()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.activity_filter, container, false)
|
||||
val recyclerView: RecyclerView =
|
||||
ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView
|
||||
val tip = ViewUtils.`$$`(view, R.id.tip) as TextView
|
||||
mViewTransition = ViewTransition(recyclerView, tip)
|
||||
val fab = view.findViewById<FloatingActionButton>(R.id.fab)
|
||||
val drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter)
|
||||
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
tip.setCompoundDrawables(null, drawable, null, null)
|
||||
mAdapter.setHasStableIds(true)
|
||||
recyclerView.adapter = mAdapter
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.clipChildren = false
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.setHasFixedSize(true)
|
||||
val defaultItemAnimator = recyclerView.itemAnimator as DefaultItemAnimator?
|
||||
defaultItemAnimator?.supportsChangeAnimations = false
|
||||
fab.setOnClickListener { showAddFilterDialog() }
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
updateView(false)
|
||||
requireActivity().addMenuProvider(mMenuProvider)
|
||||
}
|
||||
|
||||
private fun updateView(animation: Boolean) {
|
||||
if (null == mViewTransition) {
|
||||
return
|
||||
}
|
||||
if (0 == mFilterList.size()) {
|
||||
mViewTransition!!.showView(1, animation)
|
||||
} else {
|
||||
mViewTransition!!.showView(0, animation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mViewTransition = null
|
||||
requireActivity().removeMenuProvider(mMenuProvider)
|
||||
}
|
||||
|
||||
private fun showTipDialog() {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.filter)
|
||||
.setMessage(R.string.filter_tip)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showAddFilterDialog() {
|
||||
val dialog = MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.add_filter)
|
||||
.setView(R.layout.dialog_add_filter)
|
||||
.setPositiveButton(R.string.add, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
AddFilterDialogHelper(dialog)
|
||||
}
|
||||
|
||||
private fun showDeleteFilterDialog(filter: Filter) {
|
||||
val message = getString(R.string.delete_filter, filter.text)
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.delete) { _: DialogInterface?, which: Int ->
|
||||
if (DialogInterface.BUTTON_POSITIVE != which) {
|
||||
return@setPositiveButton
|
||||
}
|
||||
mFilterList.delete(filter)
|
||||
mAdapter.notifyDataSetChanged()
|
||||
updateView(true)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getFragmentTitle(): Int {
|
||||
return R.string.filter
|
||||
}
|
||||
|
||||
private class FilterHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val checkbox: MaterialCheckBox?
|
||||
val text: TextView?
|
||||
val delete: ImageView?
|
||||
|
||||
init {
|
||||
checkbox = itemView.findViewById(R.id.checkbox)
|
||||
text = itemView.findViewById(R.id.text)
|
||||
delete = itemView.findViewById(R.id.delete)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AddFilterDialogHelper(dialog: AlertDialog) : View.OnClickListener {
|
||||
private var mDialog: AlertDialog
|
||||
private var mSpinner: TextInputLayout
|
||||
private var mInputLayout: TextInputLayout
|
||||
private var mEditText: EditText
|
||||
private val mArray: Array<String> =
|
||||
context!!.resources.getStringArray(R.array.filter_entries)
|
||||
|
||||
init {
|
||||
mDialog = dialog
|
||||
mSpinner = ViewUtils.`$$`(dialog, R.id.spinner) as TextInputLayout
|
||||
mInputLayout = ViewUtils.`$$`(dialog, R.id.text_input_layout) as TextInputLayout
|
||||
mEditText = mInputLayout.editText!!
|
||||
val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
button?.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val text1 = mSpinner.editText!!.text.toString()
|
||||
if (TextUtils.isEmpty(text1)) {
|
||||
mSpinner.error = getString(R.string.text_is_empty)
|
||||
return
|
||||
} else {
|
||||
mSpinner.error = null
|
||||
}
|
||||
val text = mEditText.text.toString().trim { it <= ' ' }
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
mInputLayout.error = getString(R.string.text_is_empty)
|
||||
return
|
||||
} else {
|
||||
mInputLayout.error = null
|
||||
}
|
||||
val mode = mArray.indexOf(text1)
|
||||
val filter = Filter()
|
||||
filter.mode = mode
|
||||
filter.text = text
|
||||
if (!mFilterList.add(filter)) {
|
||||
mInputLayout.error = getString(R.string.label_text_exist)
|
||||
return
|
||||
} else {
|
||||
mInputLayout.error = null
|
||||
}
|
||||
mAdapter.notifyDataSetChanged()
|
||||
updateView(true)
|
||||
mDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FilterAdapter : RecyclerView.Adapter<FilterHolder>() {
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (mFilterList[position].mode == MODE_HEADER) {
|
||||
TYPE_HEADER
|
||||
} else {
|
||||
TYPE_ITEM
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilterHolder {
|
||||
val layoutId: Int = when (viewType) {
|
||||
TYPE_ITEM -> R.layout.item_filter
|
||||
TYPE_HEADER -> R.layout.item_filter_header
|
||||
else -> R.layout.item_filter
|
||||
}
|
||||
return FilterHolder(layoutInflater.inflate(layoutId, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FilterHolder, position: Int) {
|
||||
val filter = mFilterList[position]
|
||||
if (MODE_HEADER == filter.mode) {
|
||||
holder.text?.text = filter.text
|
||||
} else {
|
||||
holder.checkbox?.text = filter.text
|
||||
holder.checkbox?.isChecked = filter.enable
|
||||
holder.itemView.setOnClickListener {
|
||||
mFilterList.trigger(filter)
|
||||
|
||||
//for updating delete line on filter text
|
||||
mAdapter.notifyItemChanged(position)
|
||||
}
|
||||
holder.delete?.setOnClickListener { showDeleteFilterDialog(filter) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mFilterList.size()
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return run {
|
||||
val filter = mFilterList[position]
|
||||
if (filter.id != null) {
|
||||
(filter.text.hashCode() shr filter.mode) + filter.id
|
||||
} else (filter.text.hashCode() shr filter.mode).toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FilterList {
|
||||
private val mEhFilter: EhFilter = EhFilter.getInstance()
|
||||
private val mTitleFilterList: List<Filter> = mEhFilter.titleFilterList
|
||||
private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList
|
||||
private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList
|
||||
private val mTagNamespaceFilterList: List<Filter> = mEhFilter.tagNamespaceFilterList
|
||||
private var mTitleHeader: Filter? = null
|
||||
private var mUploaderHeader: Filter? = null
|
||||
private var mTagHeader: Filter? = null
|
||||
private var mTagNamespaceHeader: Filter? = null
|
||||
fun size(): Int {
|
||||
var count = 0
|
||||
var size = mTitleFilterList.size
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mUploaderFilterList.size
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mTagFilterList.size
|
||||
count += if (0 == size) 0 else size + 1
|
||||
size = mTagNamespaceFilterList.size
|
||||
count += if (0 == size) 0 else size + 1
|
||||
return count
|
||||
}
|
||||
|
||||
private val titleHeader: Filter
|
||||
get() {
|
||||
if (null == mTitleHeader) {
|
||||
mTitleHeader = Filter()
|
||||
mTitleHeader!!.mode = MODE_HEADER
|
||||
mTitleHeader!!.text = getString(R.string.filter_title)
|
||||
}
|
||||
return mTitleHeader!!
|
||||
}
|
||||
private val uploaderHeader: Filter
|
||||
get() {
|
||||
if (null == mUploaderHeader) {
|
||||
mUploaderHeader = Filter()
|
||||
mUploaderHeader!!.mode = MODE_HEADER
|
||||
mUploaderHeader!!.text = getString(R.string.filter_uploader)
|
||||
}
|
||||
return mUploaderHeader!!
|
||||
}
|
||||
private val tagHeader: Filter
|
||||
get() {
|
||||
if (null == mTagHeader) {
|
||||
mTagHeader = Filter()
|
||||
mTagHeader!!.mode = MODE_HEADER
|
||||
mTagHeader!!.text = getString(R.string.filter_tag)
|
||||
}
|
||||
return mTagHeader!!
|
||||
}
|
||||
private val tagNamespaceHeader: Filter
|
||||
get() {
|
||||
if (null == mTagNamespaceHeader) {
|
||||
mTagNamespaceHeader = Filter()
|
||||
mTagNamespaceHeader!!.mode = MODE_HEADER
|
||||
mTagNamespaceHeader!!.text = getString(R.string.filter_tag_namespace)
|
||||
}
|
||||
return mTagNamespaceHeader!!
|
||||
}
|
||||
|
||||
operator fun get(index: Int): Filter {
|
||||
var index1 = index
|
||||
var size = mTitleFilterList.size
|
||||
if (0 != size) {
|
||||
index1 -= if (index1 == 0) {
|
||||
return titleHeader
|
||||
} else if (index1 <= size) {
|
||||
return mTitleFilterList[index1 - 1]
|
||||
} else {
|
||||
size + 1
|
||||
}
|
||||
}
|
||||
size = mUploaderFilterList.size
|
||||
if (0 != size) {
|
||||
index1 -= if (index1 == 0) {
|
||||
return uploaderHeader
|
||||
} else if (index1 <= size) {
|
||||
return mUploaderFilterList[index1 - 1]
|
||||
} else {
|
||||
size + 1
|
||||
}
|
||||
}
|
||||
size = mTagFilterList.size
|
||||
if (0 != size) {
|
||||
index1 -= if (index1 == 0) {
|
||||
return tagHeader
|
||||
} else if (index1 <= size) {
|
||||
return mTagFilterList[index1 - 1]
|
||||
} else {
|
||||
size + 1
|
||||
}
|
||||
}
|
||||
size = mTagNamespaceFilterList.size
|
||||
if (0 != size) {
|
||||
if (index1 == 0) {
|
||||
return tagNamespaceHeader
|
||||
} else if (index1 <= size) {
|
||||
return mTagNamespaceFilterList[index1 - 1]
|
||||
}
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
fun add(filter: Filter?): Boolean {
|
||||
return mEhFilter.addFilter(filter)
|
||||
}
|
||||
|
||||
fun delete(filter: Filter?) {
|
||||
mEhFilter.deleteFilter(filter)
|
||||
}
|
||||
|
||||
fun trigger(filter: Filter?) {
|
||||
mEhFilter.triggerFilter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MODE_HEADER = -1
|
||||
private const val TYPE_ITEM = 0
|
||||
private const val TYPE_HEADER = 1
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.app.Dialog;
|
||||
@@ -16,21 +34,18 @@ import androidx.fragment.app.DialogFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.Hosts;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.view.ViewTransition;
|
||||
import com.hippo.yorozuya.LayoutUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
|
||||
public class HostsFragment extends BaseFragment
|
||||
implements View.OnClickListener {
|
||||
|
||||
@@ -66,12 +81,6 @@ public class HostsFragment extends BaseFragment
|
||||
adapter = new HostsAdapter();
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity(), RecyclerView.VERTICAL, false));
|
||||
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
|
||||
LinearDividerItemDecoration.VERTICAL,
|
||||
ResourcesKt.resolveColor(requireActivity().getTheme(), R.attr.dividerColor),
|
||||
LayoutUtils.dp2pix(requireActivity(), 1));
|
||||
decoration.setShowLastDivider(true);
|
||||
recyclerView.addItemDecoration(decoration);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
|
||||
fab.setOnClickListener(this);
|
||||
@@ -117,6 +126,11 @@ public class HostsFragment extends BaseFragment
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.hosts;
|
||||
}
|
||||
|
||||
public abstract static class HostDialogFragment extends DialogFragment {
|
||||
|
||||
private HostsFragment hostsFragment;
|
||||
@@ -135,7 +149,7 @@ public class HostsFragment extends BaseFragment
|
||||
ip.setText(arguments.getString(KEY_IP));
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()).setView(view);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()).setView(view);
|
||||
onCreateDialogBuilder(builder);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.setOnShowListener(d -> onCreateDialog((AlertDialog) d));
|
||||
@@ -265,9 +279,4 @@ public class HostsFragment extends BaseFragment
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.hosts;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -72,6 +90,11 @@ public class MyTagsFragment extends BaseFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.my_tags;
|
||||
}
|
||||
|
||||
private class MyTagsWebViewClient extends WebViewClient {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
@@ -89,9 +112,4 @@ public class MyTagsFragment extends BaseFragment {
|
||||
progress.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.my_tags;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.Analytics;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
/**
|
||||
* Created by Mo10 on 2018/2/10.
|
||||
*/
|
||||
|
||||
public class PrivacyFragment extends BasePreferenceFragment {
|
||||
private static final String KEY_PATTERN_PROTECTION = "pattern_protection";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.privacy_settings);
|
||||
Preference enableAnalytics = findPreference(Settings.KEY_ENABLE_ANALYTICS);
|
||||
|
||||
enableAnalytics.setOnPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Preference patternProtection = findPreference(KEY_PATTERN_PROTECTION);
|
||||
patternProtection.setSummary(TextUtils.isEmpty(Settings.getSecurity()) ?
|
||||
R.string.settings_privacy_pattern_protection_not_set :
|
||||
R.string.settings_privacy_pattern_protection_set);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
String key = preference.getKey();
|
||||
if (Settings.KEY_ENABLE_ANALYTICS.equals(key)) {
|
||||
if (newValue instanceof Boolean && (Boolean) newValue) {
|
||||
Analytics.start(getActivity().getApplication());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.settings_privacy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.ui.scene.SecurityScene.Companion.isAuthenticationSupported
|
||||
|
||||
/**
|
||||
* Created by Mo10 on 2018/2/10.
|
||||
*/
|
||||
class PrivacyFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.privacy_settings)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (!isAuthenticationSupported(requireContext())) {
|
||||
Settings.putSecurity(false)
|
||||
findPreference<SwitchPreferenceCompat>("require_unlock")?.isEnabled = false
|
||||
findPreference<SwitchPreferenceCompat>("require_unlock")?.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFragmentTitle(): Int {
|
||||
return R.string.settings_privacy
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import com.hippo.ehviewer.R;
|
||||
public class ReadFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.read_settings);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.biometric.BiometricManager;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.widget.lockpattern.LockPatternUtils;
|
||||
import com.hippo.widget.lockpattern.LockPatternView;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class SetSecurityFragment extends BaseFragment implements View.OnClickListener {
|
||||
|
||||
@Nullable
|
||||
private LockPatternView mPatternView;
|
||||
@Nullable
|
||||
private View mCancel;
|
||||
@Nullable
|
||||
private View mSet;
|
||||
@Nullable
|
||||
private CheckBox mFingerprint;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.activity_set_security, container, false);
|
||||
|
||||
mPatternView = (LockPatternView) ViewUtils.$$(view, R.id.pattern_view);
|
||||
mCancel = ViewUtils.$$(view, R.id.cancel);
|
||||
mSet = ViewUtils.$$(view, R.id.set);
|
||||
mFingerprint = (CheckBox) ViewUtils.$$(view, R.id.fingerprint_checkbox);
|
||||
|
||||
String pattern = Settings.getSecurity();
|
||||
if (!TextUtils.isEmpty(pattern)) {
|
||||
mPatternView.setPattern(LockPatternView.DisplayMode.Correct,
|
||||
LockPatternUtils.stringToPattern(pattern));
|
||||
}
|
||||
|
||||
if (BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
mFingerprint.setVisibility(View.VISIBLE);
|
||||
mFingerprint.setChecked(Settings.getEnableFingerprint());
|
||||
}
|
||||
|
||||
mCancel.setOnClickListener(this);
|
||||
mSet.setOnClickListener(this);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
mPatternView = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == mCancel) {
|
||||
requireActivity().onBackPressed();
|
||||
} else if (v == mSet) {
|
||||
if (null != mPatternView && null != mFingerprint) {
|
||||
String security;
|
||||
if (mPatternView.getCellSize() <= 1) {
|
||||
security = "";
|
||||
} else {
|
||||
security = mPatternView.getPatternString();
|
||||
}
|
||||
Settings.putSecurity(security);
|
||||
Settings.putEnableFingerprint(mFingerprint.getVisibility() == View.VISIBLE &&
|
||||
mFingerprint.isChecked() && !security.isEmpty());
|
||||
}
|
||||
requireActivity().onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.set_pattern_protection;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
@@ -8,7 +26,7 @@ import com.hippo.ehviewer.R;
|
||||
|
||||
public class SettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.settings_headers);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -146,6 +164,16 @@ public class UConfigFragment extends BaseFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.u_config;
|
||||
}
|
||||
|
||||
private class UConfigWebViewClient extends WebViewClient {
|
||||
|
||||
@Override
|
||||
@@ -166,14 +194,4 @@ public class UConfigFragment extends BaseFragment {
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.u_config;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.scene;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.Analytics;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class AnalyticsScene extends SolidScene implements View.OnClickListener {
|
||||
|
||||
@Nullable
|
||||
private View mReject;
|
||||
@Nullable
|
||||
private View mAccept;
|
||||
|
||||
@Override
|
||||
public boolean needShowLeftDrawer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.scene_analytics, container, false);
|
||||
|
||||
mReject = ViewUtils.$$(view, R.id.reject);
|
||||
mAccept = ViewUtils.$$(view, R.id.accept);
|
||||
TextView text = (TextView) ViewUtils.$$(view, R.id.text);
|
||||
|
||||
text.setText(Html.fromHtml(getString(R.string.analytics_explain)));
|
||||
text.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
mReject.setOnClickListener(this);
|
||||
mAccept.setOnClickListener(this);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
mReject = null;
|
||||
mAccept = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Context context = getContext();
|
||||
MainActivity activity = getMainActivity();
|
||||
if (context == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mReject == v) {
|
||||
Settings.putEnableAnalytics(false);
|
||||
} else if (mAccept == v) {
|
||||
Settings.putEnableAnalytics(true);
|
||||
}
|
||||
Analytics.setEnabled(Settings.getEnableAnalytics());
|
||||
Settings.putAskAnalytics(false);
|
||||
|
||||
// Start new scene and finish it self
|
||||
startSceneForCheckStep(CHECK_STEP_ANALYTICS, getArguments());
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -39,10 +39,8 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.hippo.ehviewer.Analytics;
|
||||
import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.scene.SceneFragment;
|
||||
import com.hippo.util.AppHelper;
|
||||
|
||||
public abstract class BaseScene extends SceneFragment {
|
||||
|
||||
@@ -60,13 +58,6 @@ public abstract class BaseScene extends SceneFragment {
|
||||
private SparseArray<Parcelable> drawerViewState;
|
||||
private boolean needWhiteStatusBar = needWhiteStatusBar();
|
||||
|
||||
public void updateAvatar() {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
((MainActivity) activity).updateProfile();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAboveSnackView(View view) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
@@ -274,12 +265,6 @@ public abstract class BaseScene extends SceneFragment {
|
||||
return insetsController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Analytics.onSceneView(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
@@ -30,8 +30,8 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -217,7 +217,7 @@ public class CookieSignInScene extends SolidScene implements EditText.OnEditorAc
|
||||
hideSoftInput();
|
||||
|
||||
if (!checkIpbMemberId(ipbMemberId) || !(checkIpbPassHash(ipbPassHash))) {
|
||||
new AlertDialog.Builder(context).setTitle(R.string.waring)
|
||||
new MaterialAlertDialogBuilder(context).setTitle(R.string.waring)
|
||||
.setMessage(R.string.wrong_cookie_warning)
|
||||
.setPositiveButton(R.string.i_will_check_it, null)
|
||||
.setNegativeButton(R.string.i_dont_think_so, (dialog, which) -> {
|
||||
|
||||
@@ -30,7 +30,6 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -58,6 +57,7 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.getkeepsafe.taptargetview.TapTarget;
|
||||
import com.getkeepsafe.taptargetview.TapTargetView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
|
||||
@@ -72,7 +72,6 @@ import com.hippo.conaco.ProgressNotifier;
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView;
|
||||
import com.hippo.easyrecyclerview.FastScroller;
|
||||
import com.hippo.easyrecyclerview.HandlerDrawable;
|
||||
import com.hippo.easyrecyclerview.LinearDividerItemDecoration;
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
@@ -113,7 +112,6 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public class DownloadsScene extends ToolbarScene
|
||||
@@ -377,12 +375,10 @@ public class DownloadsScene extends ToolbarScene
|
||||
mRecyclerView.scrollToPosition(mInitPosition);
|
||||
mInitPosition = -1;
|
||||
}
|
||||
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 4,4,4,4, TypedValue.COMPLEX_UNIT_DIP);
|
||||
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
|
||||
|
||||
mFastScroller.attachToRecyclerView(mRecyclerView);
|
||||
HandlerDrawable handlerDrawable = new HandlerDrawable();
|
||||
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), R.attr.widgetColorThemeAccent));
|
||||
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
|
||||
mFastScroller.setHandlerDrawable(handlerDrawable);
|
||||
mFastScroller.setOnDragHandlerListener(this);
|
||||
|
||||
@@ -436,8 +432,8 @@ public class DownloadsScene extends ToolbarScene
|
||||
|
||||
TapTargetView.showFor(requireActivity(),
|
||||
TapTarget.forView(((DownloadHolder) holder).thumb,
|
||||
getString(R.string.guide_download_thumb_title),
|
||||
getString(R.string.guide_download_thumb_text))
|
||||
getString(R.string.guide_download_thumb_title),
|
||||
getString(R.string.guide_download_thumb_text))
|
||||
.transparentTarget(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
@@ -465,9 +461,8 @@ public class DownloadsScene extends ToolbarScene
|
||||
|
||||
TapTargetView.showFor(requireActivity(),
|
||||
TapTarget.forBounds(bounds,
|
||||
getString(R.string.guide_download_labels_title),
|
||||
getString(R.string.guide_download_labels_text))
|
||||
.outerCircleColor(R.color.colorPrimary)
|
||||
getString(R.string.guide_download_labels_title),
|
||||
getString(R.string.guide_download_labels_text))
|
||||
.transparentTarget(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
@@ -525,7 +520,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_filter) {
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setSingleChoiceItems(R.array.download_state, mType + 1, (dialog, which) -> {
|
||||
mType = which - 1;
|
||||
updateForLabel();
|
||||
@@ -548,7 +543,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
openDrawer(Gravity.RIGHT);
|
||||
return true;
|
||||
} else if (id == R.id.action_reset_reading_progress) {
|
||||
new AlertDialog.Builder(getContext())
|
||||
new MaterialAlertDialogBuilder(getContext())
|
||||
.setMessage(R.string.reset_reading_progress_message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
@@ -621,7 +616,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
for (int i = 0, n = list.size(); i < n; i++) {
|
||||
items[i + 2] = list.get(i).getLabel();
|
||||
}
|
||||
new AlertDialog.Builder(context)
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.default_download_label)
|
||||
.setItems(items, (dialog1, which) -> {
|
||||
if (which == 0) {
|
||||
@@ -652,12 +647,6 @@ public class DownloadsScene extends ToolbarScene
|
||||
mLabelAdapter = new DownloadLabelAdapter(inflater);
|
||||
final EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
LinearDividerItemDecoration decoration = new LinearDividerItemDecoration(
|
||||
LinearDividerItemDecoration.VERTICAL,
|
||||
ResourcesKt.resolveColor(getTheme(), R.attr.dividerColor),
|
||||
LayoutUtils.dp2pix(context, 1));
|
||||
decoration.setShowLastDivider(true);
|
||||
recyclerView.addItemDecoration(decoration);
|
||||
mLabelAdapter.setHasStableIds(true);
|
||||
final GeneralItemAnimator animator = new DraggableItemAnimator();
|
||||
recyclerView.setItemAnimator(animator);
|
||||
@@ -821,7 +810,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
|
||||
MoveDialogHelper helper = new MoveDialogHelper(labels, downloadInfoList);
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.download_move_dialog_title)
|
||||
.setItems(labels, helper)
|
||||
.show();
|
||||
@@ -998,6 +987,73 @@ public class DownloadsScene extends ToolbarScene
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThumbDataContainer implements DataContainer {
|
||||
|
||||
private final DownloadInfo mInfo;
|
||||
@Nullable
|
||||
private UniFile mFile;
|
||||
|
||||
public ThumbDataContainer(@NonNull DownloadInfo info) {
|
||||
mInfo = info;
|
||||
}
|
||||
|
||||
private void ensureFile() {
|
||||
if (mFile == null) {
|
||||
UniFile dir = SpiderDen.getGalleryDownloadDir(mInfo);
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
mFile = dir.createFile(".thumb");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
ensureFile();
|
||||
return mFile != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUrlMoved(String requestUrl, String responseUrl) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(InputStream is, long length, String mediaType, ProgressNotifier notify) {
|
||||
ensureFile();
|
||||
if (mFile == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = mFile.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamPipe get() {
|
||||
ensureFile();
|
||||
if (mFile != null) {
|
||||
return new UniFileInputStreamPipe(mFile);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (mFile != null) {
|
||||
mFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadLabelAdapter extends RecyclerView.Adapter<DownloadLabelHolder> implements DraggableItemAdapter<DownloadLabelHolder> {
|
||||
|
||||
private final LayoutInflater mInflater;
|
||||
@@ -1067,7 +1123,7 @@ public class DownloadsScene extends ToolbarScene
|
||||
AlertDialog dialog = builder.show();
|
||||
new RenameLabelDialogHelper(builder, dialog, label);
|
||||
} else if (itemId == R.id.menu_label_remove) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.delete_label_title))
|
||||
.setMessage(getString(R.string.delete_label_message, label))
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
@@ -1277,7 +1333,6 @@ public class DownloadsScene extends ToolbarScene
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list.get(index));
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
|
||||
startScene(announcer);
|
||||
} else if (start == v) {
|
||||
Intent intent = new Intent(activity, DownloadService.class);
|
||||
@@ -1391,73 +1446,6 @@ public class DownloadsScene extends ToolbarScene
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThumbDataContainer implements DataContainer {
|
||||
|
||||
private final DownloadInfo mInfo;
|
||||
@Nullable
|
||||
private UniFile mFile;
|
||||
|
||||
public ThumbDataContainer(@NonNull DownloadInfo info) {
|
||||
mInfo = info;
|
||||
}
|
||||
|
||||
private void ensureFile() {
|
||||
if (mFile == null) {
|
||||
UniFile dir = SpiderDen.getGalleryDownloadDir(mInfo);
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
mFile = dir.createFile(".thumb");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
ensureFile();
|
||||
return mFile != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUrlMoved(String requestUrl, String responseUrl) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(InputStream is, long length, String mediaType, ProgressNotifier notify) {
|
||||
ensureFile();
|
||||
if (mFile == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = mFile.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStreamPipe get() {
|
||||
ensureFile();
|
||||
if (mFile != null) {
|
||||
return new UniFileInputStreamPipe(mFile);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (mFile != null) {
|
||||
mFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RenameLabelDialogHelper implements View.OnClickListener {
|
||||
|
||||
private final EditTextDialogBuilder mBuilder;
|
||||
|
||||
Loaded 100 of 621 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user