Compare commits
412 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ac4435d4 | ||
|
|
1b9dfe1901 | ||
|
|
808a6efd8f | ||
|
|
2ce5cb524f | ||
|
|
4cbc6392fb | ||
|
|
049f9fa625 | ||
|
|
c853fae820 | ||
|
|
dd1d84a4fe | ||
|
|
1569aa5dd5 | ||
|
|
51cd88eded | ||
|
|
bf386deef0 | ||
|
|
5c80cdee81 | ||
|
|
b29fbb37cd | ||
|
|
589831beef | ||
|
|
0f5d153543 | ||
|
|
ab1c99d132 | ||
|
|
af4870c39c | ||
|
|
9581b3da65 | ||
|
|
358a907b74 | ||
|
|
cdb1d8fe12 | ||
|
|
5513382aea | ||
|
|
a7a5c8978d | ||
|
|
b139d5fca5 | ||
|
|
977da5b1b4 | ||
|
|
78f2a13761 | ||
|
|
7ded7fd12a | ||
|
|
e50f79a25e | ||
|
|
904fc572d0 | ||
|
|
c280af9a5b | ||
|
|
af6df6dfa2 | ||
|
|
2380d69b11 | ||
|
|
ad76d6d414 | ||
|
|
d911ee12f2 | ||
|
|
e6ce03b516 | ||
|
|
17d7deef2d | ||
|
|
c2222344a2 | ||
|
|
0a8d677fe8 | ||
|
|
a4e1381238 | ||
|
|
1b6837d406 | ||
|
|
2adf8a139c | ||
|
|
efe96a6e05 | ||
|
|
0360df999f | ||
|
|
5cb4758b38 | ||
|
|
fce9f543e1 | ||
|
|
9e6cb1837e | ||
|
|
f9e40e17c4 | ||
|
|
a09d71cb13 | ||
|
|
de53445ac5 | ||
|
|
19fdd54dbd | ||
|
|
1c644188cd | ||
|
|
a50943ed01 | ||
|
|
1888aba335 | ||
|
|
b390fd49ca | ||
|
|
33f0eb9f38 | ||
|
|
db91458abc | ||
|
|
ee2ed0159d | ||
|
|
0bdc3e024e | ||
|
|
4aab4e636d | ||
|
|
af114d74df | ||
|
|
c5921f8a62 | ||
|
|
be0c8f2c96 | ||
|
|
bb685751cd | ||
|
|
fd9737aa9c | ||
|
|
1dcb479d62 | ||
|
|
7618a05162 | ||
|
|
174c6649e0 | ||
|
|
203608e9fd | ||
|
|
07e0ae884c | ||
|
|
cd8e256364 | ||
|
|
93998e460c | ||
|
|
71f205ca8b | ||
|
|
f9cee7a8f5 | ||
|
|
675e95da2b | ||
|
|
c1b6cef362 | ||
|
|
4977464e69 | ||
|
|
593624fdb9 | ||
|
|
c4585c81e1 | ||
|
|
27293f1bf8 | ||
|
|
d1fd31701d | ||
|
|
d30c7e6e9c | ||
|
|
0355b61e69 | ||
|
|
5d5ec719b7 | ||
|
|
6596dca291 | ||
|
|
a296c98602 | ||
|
|
be0718acf4 | ||
|
|
c42d913d4c | ||
|
|
446649b2bb | ||
|
|
9f145557ea | ||
|
|
ae856fca74 | ||
|
|
6dc8a4ffb5 | ||
|
|
73498964a8 | ||
|
|
256f88cc60 | ||
|
|
16cc6fb117 | ||
|
|
7bb809f227 | ||
|
|
57111f628d | ||
|
|
0129e9e092 | ||
|
|
d6c6132a04 | ||
|
|
eb5976a796 | ||
|
|
253f4abba1 | ||
|
|
3a442817ce | ||
|
|
594c359f1c | ||
|
|
cc28d4fe54 | ||
|
|
95708367a1 | ||
|
|
89b915b206 | ||
|
|
e4da0a126c | ||
|
|
56f9cc2c88 | ||
|
|
6037c66a2d | ||
|
|
d25837b40b | ||
|
|
fbd0f25b8f | ||
|
|
9c55fd166e | ||
|
|
2ac1828a0c | ||
|
|
45e1502c9b | ||
|
|
e2608cf85a | ||
|
|
05bbfe77b2 | ||
|
|
34ad0a7c68 | ||
|
|
c67ce38350 | ||
|
|
ad79ff2739 | ||
|
|
2e5afc73e7 | ||
|
|
73efe6fd83 | ||
|
|
c59e3165b6 | ||
|
|
ec8c5e0fd4 | ||
|
|
149ac9280c | ||
|
|
af20f65468 | ||
|
|
6f7efa9e26 | ||
|
|
7f5ef227eb | ||
|
|
e8e95a485b | ||
|
|
77186d271d | ||
|
|
ebeaf9703f | ||
|
|
625b2769c6 | ||
|
|
52e136ddef | ||
|
|
78fe18735b | ||
|
|
2f89c0bb92 | ||
|
|
fbac8881ce | ||
|
|
b51b3460c0 | ||
|
|
aaea4147a4 | ||
|
|
d2609c0560 | ||
|
|
6a3421df8a | ||
|
|
86be393335 | ||
|
|
96d6f9d80d | ||
|
|
384d0345f5 | ||
|
|
eb780a1449 | ||
|
|
15d094a175 | ||
|
|
eba5e484d6 | ||
|
|
7402e8569a | ||
|
|
8ae7863185 | ||
|
|
75b9fd1b7a | ||
|
|
dc46657fa6 | ||
|
|
d77177bbfd | ||
|
|
7c6a97e264 | ||
|
|
23f84c2416 | ||
|
|
d41a813e41 | ||
|
|
fae958f6ef | ||
|
|
d8db89326f | ||
|
|
3804896788 | ||
|
|
4aedea7e15 | ||
|
|
ed89d76488 | ||
|
|
bbd43b51e3 | ||
|
|
e5448fa8ab | ||
|
|
a6a392c7bf | ||
|
|
08f92f9614 | ||
|
|
c9cf09f4dd | ||
|
|
ab1624c918 | ||
|
|
62396111e3 | ||
|
|
e37f6f31da | ||
|
|
873b41e4f9 | ||
|
|
f0d4deffd7 | ||
|
|
b6c50d59ed | ||
|
|
9fcc19ef7e | ||
|
|
b90ebdabf9 | ||
|
|
e08a4cf1b2 | ||
|
|
bd4efcf110 | ||
|
|
0b9013e8b2 | ||
|
|
445128f462 | ||
|
|
f50a8b3112 | ||
|
|
ed65145f83 | ||
|
|
49e08eaf2f | ||
|
|
83a9570961 | ||
|
|
973a4073f0 | ||
|
|
867812b8e3 | ||
|
|
cf7341b065 | ||
|
|
de3c4545e6 | ||
|
|
a483d21120 | ||
|
|
665d46b7c4 | ||
|
|
3d5a1e9b30 | ||
|
|
e7e9c5fe9f | ||
|
|
c71460fcd8 | ||
|
|
8c2bc078e5 | ||
|
|
75b1068d46 | ||
|
|
4ac406aa2d | ||
|
|
f4f367850e | ||
|
|
b293e9f370 | ||
|
|
4e2350e5fc | ||
|
|
09412719b7 | ||
|
|
cd7d6d7674 | ||
|
|
bc0c5ac71a | ||
|
|
91619cc259 | ||
|
|
f0c9c61b49 | ||
|
|
d65158b7b9 | ||
|
|
4e5de1e33e | ||
|
|
b009a6423d | ||
|
|
467d0c8e18 | ||
|
|
4d535cef41 | ||
|
|
98147d0a81 | ||
|
|
323c1defaa | ||
|
|
60c5408ae8 | ||
|
|
51dc2ac046 | ||
|
|
24d9a49420 | ||
|
|
46891aa958 | ||
|
|
d1921193f0 | ||
|
|
a8c4c4045c | ||
|
|
0559c13dc6 | ||
|
|
3fbec046ba | ||
|
|
4c8fa91af4 | ||
|
|
1568c09fa2 | ||
|
|
0e74d6e017 | ||
|
|
7690a29efb | ||
|
|
b1d6f5debd | ||
|
|
fbb92005a1 | ||
|
|
5b9922d509 | ||
|
|
49eebdf554 | ||
|
|
2da941d550 | ||
|
|
5a8d7531bf | ||
|
|
4d1f5e22d3 | ||
|
|
012416c881 | ||
|
|
0f48ad07a3 | ||
|
|
64752da948 | ||
|
|
95148a1071 | ||
|
|
d9d0656ef4 | ||
|
|
b17d8efa5c | ||
|
|
b07fcf5842 | ||
|
|
f669a1ca0f | ||
|
|
5a921ea862 | ||
|
|
810efde0b0 | ||
|
|
03510a1f19 | ||
|
|
049f32d2f0 | ||
|
|
11a9db3cc2 | ||
|
|
57dd5743f0 | ||
|
|
5f37e76c85 | ||
|
|
fc51d49505 | ||
|
|
3dde254452 | ||
|
|
aa21dd9721 | ||
|
|
71f5ee8cb1 | ||
|
|
40f27ae634 | ||
|
|
0dfba47d85 | ||
|
|
69e44b10e9 | ||
|
|
4cd0cb04a3 | ||
|
|
d9d5595bde | ||
|
|
ed70ca4e18 | ||
|
|
a371bb6514 | ||
|
|
ee26a3e434 | ||
|
|
5bb6eae673 | ||
|
|
3357c00578 | ||
|
|
1f2f40f077 | ||
|
|
c25ee93ccb | ||
|
|
4aa1b58109 | ||
|
|
c64115a268 | ||
|
|
33296217a4 | ||
|
|
0e384c134d | ||
|
|
7f37c1f99e | ||
|
|
d1aa0f0407 | ||
|
|
4d904fe12f | ||
|
|
3df8b8d170 | ||
|
|
42f0fa9bbf | ||
|
|
5cbc592d23 | ||
|
|
85c424580a | ||
|
|
0d0e3acd04 | ||
|
|
49f9fb0488 | ||
|
|
bbcd96b981 | ||
|
|
510c5b70c9 | ||
|
|
951a0db3f2 | ||
|
|
d85f23b320 | ||
|
|
0c0214a85e | ||
|
|
9054f5720f | ||
|
|
bb1dd74277 | ||
|
|
96d437b2a8 | ||
|
|
8f8d85d172 | ||
|
|
a242aa6633 | ||
|
|
1a0986212b | ||
|
|
22e7bab879 | ||
|
|
9bd7daef65 | ||
|
|
d1e17c8ec2 | ||
|
|
e674e0f36f | ||
|
|
7fd71c13f3 | ||
|
|
9a0b7c4700 | ||
|
|
c54d128c09 | ||
|
|
a1545fd889 | ||
|
|
6e1fdcb19a | ||
|
|
72bedfd92e | ||
|
|
c132f1d5c4 | ||
|
|
abc4ab92a9 | ||
|
|
0931e4e0e6 | ||
|
|
113cde2f07 | ||
|
|
bf2d82723b | ||
|
|
6463023736 | ||
|
|
b8d2fa69c4 | ||
|
|
904d12f611 | ||
|
|
71a5801a0c | ||
|
|
6b529f806f | ||
|
|
9b5510ac59 | ||
|
|
90be936c82 | ||
|
|
29e6eab0e7 | ||
|
|
75b3ea0bc9 | ||
|
|
a215d9ebfc | ||
|
|
cef5d91eec | ||
|
|
9c20559962 | ||
|
|
b1be45af8b | ||
|
|
5ed4d0b6b7 | ||
|
|
53e36d23b1 | ||
|
|
fa02cfd7e8 | ||
|
|
1b1540b35b | ||
|
|
b9f35f34ad | ||
|
|
12c8cdfd70 | ||
|
|
971f708e45 | ||
|
|
7e76e10591 | ||
|
|
7d24286c55 | ||
|
|
eaac271143 | ||
|
|
d135898b49 | ||
|
|
03dbd86363 | ||
|
|
908baebb62 | ||
|
|
5190ec3e98 | ||
|
|
17c20b2bf9 | ||
|
|
e2b65f6fb6 | ||
|
|
28a9659410 | ||
|
|
bdebd0578e | ||
|
|
53542f3f86 | ||
|
|
2772f0b3dd | ||
|
|
95a4bf41d2 | ||
|
|
e497781359 | ||
|
|
72fdc7796f | ||
|
|
a885709ba9 | ||
|
|
578c1c3825 | ||
|
|
a5fba83510 | ||
|
|
6f3ae19345 | ||
|
|
2135195f27 | ||
|
|
a8c22de601 | ||
|
|
56e145420c | ||
|
|
fb60b26f08 | ||
|
|
ff3ebbf1d9 | ||
|
|
4dc9df0515 | ||
|
|
e9bce8ef15 | ||
|
|
55fc1aeadd | ||
|
|
693f568b8e | ||
|
|
5293a8d209 | ||
|
|
1c46fc7f23 | ||
|
|
b7e4c6b8c0 | ||
|
|
df599e9d50 | ||
|
|
6009f089e7 | ||
|
|
0a4f2f848e | ||
|
|
85fc3a024c | ||
|
|
eeb536b1ac | ||
|
|
5b8e8d76c0 | ||
|
|
73cf2964b2 | ||
|
|
8372f9b5de | ||
|
|
d7181e35e7 | ||
|
|
55d824bb94 | ||
|
|
229d9fa2ae | ||
|
|
3eb68e1ff9 | ||
|
|
b103589bba | ||
|
|
0726c037a4 | ||
|
|
0ff64931e0 | ||
|
|
2374c96009 | ||
|
|
2dd51117e9 | ||
|
|
6c5f3c7d97 | ||
|
|
626bb20edb | ||
|
|
d363869dab | ||
|
|
774f33c63d | ||
|
|
079427346a | ||
|
|
a1a3125834 | ||
|
|
fc9c8f8a79 | ||
|
|
c06923dbdf | ||
|
|
66ca51cc73 | ||
|
|
bf45480366 | ||
|
|
28618e394e | ||
|
|
9762a466ce | ||
|
|
367a97a95b | ||
|
|
c3ab197aa0 | ||
|
|
a0aa33a499 | ||
|
|
b27bc86141 | ||
|
|
84ef2af82f | ||
|
|
a2f09d8763 | ||
|
|
79058440a1 | ||
|
|
7f9cfdbf7a | ||
|
|
85f7477450 | ||
|
|
0e08d75626 | ||
|
|
1b4a65f476 | ||
|
|
2e69395ade | ||
|
|
3f61f13b7b | ||
|
|
10a0f0ad53 | ||
|
|
680fc66f21 | ||
|
|
e01b74ee3d | ||
|
|
3539e6a892 | ||
|
|
ff56f5a343 | ||
|
|
9ce43a39c8 | ||
|
|
0e3aa3f380 | ||
|
|
7927bf0c9a | ||
|
|
aec2d71688 | ||
|
|
140a0f4d66 | ||
|
|
7cf57535ab | ||
|
|
31fe924157 | ||
|
|
6d193baa69 | ||
|
|
3bd7b54405 | ||
|
|
d99450c5a3 | ||
|
|
6444122c0a | ||
|
|
fe14ccb5ec | ||
|
|
e38e5fdf0f | ||
|
|
c1c2b11bd8 | ||
|
|
7d147b3c37 | ||
|
|
260ff32cd1 | ||
|
|
ccc5f3e423 | ||
|
|
8b32a60743 | ||
|
|
c1faf2fe06 | ||
|
|
3588270742 |
2
.gitignore
vendored
@@ -3,7 +3,9 @@
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/dictionaries
|
||||
/.idea/modules.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
|
||||
1
.idea/codeStyles/Project.xml
generated
@@ -23,6 +23,7 @@
|
||||
</option>
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="CMake">
|
||||
|
||||
4
.idea/compiler.xml
generated
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel>
|
||||
<module name="Kotatsu.app" target="1.8" />
|
||||
</bytecodeTargetLevel>
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
17
.idea/deploymentTargetDropDown.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/dictionaries/admin.xml
generated
@@ -1,13 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="admin">
|
||||
<words>
|
||||
<w>chucker</w>
|
||||
<w>desu</w>
|
||||
<w>koin</w>
|
||||
<w>kotatsu</w>
|
||||
<w>manga</w>
|
||||
<w>upsert</w>
|
||||
<w>webtoon</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
6
.idea/gradle.xml
generated
@@ -4,18 +4,16 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="gradleJvm" value="Embedded JDK" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
2
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -2,6 +2,8 @@
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
5
.idea/jarRepositories.xml
generated
@@ -31,5 +31,10 @@
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
14
.travis.yml
@@ -1,15 +1,11 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- android-30
|
||||
- build-tools-30.0.3
|
||||
- platform-tools-30.0.5
|
||||
- tools
|
||||
- platform-tools-29.0.6
|
||||
- build-tools-29.0.3
|
||||
- android-29
|
||||
licenses:
|
||||
- android-sdk-preview-license-.+
|
||||
- android-sdk-license-.+
|
||||
- google-gdk-license-.+
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-30"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
||||
29
README.md
@@ -2,31 +2,38 @@
|
||||
|
||||
Kotatsu is a free and open source manga reader for Android.
|
||||
|
||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||
|
||||
### Download
|
||||
|
||||
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
|
||||
|
||||
Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/releases/tag/v0.3-legacy)
|
||||
Download APK from Github Releases:
|
||||
|
||||
- [Latest release](https://github.com/nv95/Kotatsu/releases/latest)
|
||||
- [Legacy build](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy) (with Android 4.1+ support)
|
||||
|
||||
### Main Features
|
||||
|
||||
* Online manga catalogues
|
||||
* Search manga by name and genre
|
||||
* Reading history
|
||||
* Favourites with custom categories
|
||||
* Saving manga and reading it offline
|
||||
* Tablet-optimized modern UI
|
||||
* Reading third-party comics from CBZ
|
||||
* Favourites organized by user-defined categories
|
||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
||||
* Tablet-optimized material design UI
|
||||
* Standard and Webtoon-optimized reader
|
||||
* Notifications about new chapters
|
||||
* Notifications about new chapters with updates feed
|
||||
|
||||
### Screenshots
|
||||
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
|  |  |
|
||||
|---|---|
|
||||
|
||||
### License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
@@ -39,4 +46,4 @@ published by the Free Software Foundation, either version 3 of the License, or
|
||||
|
||||
### Disclaimer
|
||||
|
||||
The developers of this application does not have any affiliation with the content providers available.
|
||||
The developers of this application does not have any affiliation with the content providers available.
|
||||
|
||||
116
app/build.gradle
@@ -1,25 +1,22 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-android-extensions'
|
||||
id 'kotlin-kapt'
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion '30.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode gitCommits
|
||||
versionName '0.3.2'
|
||||
|
||||
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||
targetSdkVersion 31
|
||||
versionCode 373
|
||||
versionName '2.0.1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
@@ -31,9 +28,6 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = '.debug'
|
||||
@@ -45,60 +39,80 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
abortOnError false
|
||||
}
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.returnDefaultValues = false
|
||||
}
|
||||
}
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += [
|
||||
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
]
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.3.0-rc01'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
|
||||
implementation 'androidx.activity:activity-ktx:1.2.0-alpha04'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha04'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta5'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.6'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.3.4'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha06'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.7.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.2.5'
|
||||
implementation 'androidx.room:room-ktx:2.2.5'
|
||||
kapt 'androidx.room:room-compiler:2.2.5'
|
||||
implementation 'androidx.room:room-runtime:2.3.0'
|
||||
implementation 'androidx.room:room-ktx:2.3.0'
|
||||
kapt 'androidx.room:room-compiler:2.3.0'
|
||||
|
||||
implementation 'com.github.moxy-community:moxy:2.1.2'
|
||||
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
|
||||
implementation 'com.github.moxy-community:moxy-material:2.1.2'
|
||||
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
|
||||
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
implementation 'com.squareup.okio:okio:2.10.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.6.0'
|
||||
implementation 'com.squareup.okio:okio:2.6.0'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.1'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.1'
|
||||
|
||||
implementation 'org.koin:koin-android:2.1.5'
|
||||
implementation 'io.coil-kt:coil:0.10.1'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
implementation 'com.tomclaw.cache:cache:1.0'
|
||||
implementation 'io.insert-koin:koin-android:3.1.3'
|
||||
implementation 'io.coil-kt:coil-base:1.4.0'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.3'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
||||
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.2.0'
|
||||
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.2.0'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.json:json:20190722'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'com.google.truth:truth:1.1.3'
|
||||
testImplementation 'org.json:json:20210307'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
|
||||
testImplementation 'io.insert-koin:koin-test-junit4:3.1.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
||||
androidTestImplementation 'androidx.room:room-testing:2.3.0'
|
||||
androidTestImplementation 'com.google.truth:truth:1.1.3'
|
||||
}
|
||||
4
app/proguard-rules.pro
vendored
@@ -5,9 +5,7 @@
|
||||
public static void checkReturnedValueIsNotNull(...);
|
||||
public static void checkFieldIsNotNull(...);
|
||||
public static void checkParameterIsNotNull(...);
|
||||
public static void checkNotNullParameter(...);
|
||||
}
|
||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
|
||||
public <init>(...);
|
||||
}
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koitharu.kotatsu.core.db.migrations.*
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MangaDatabaseTest {
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
MangaDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrateAll() {
|
||||
helper.createDatabase(TEST_DB, 1).apply {
|
||||
// TODO execSQL("")
|
||||
close()
|
||||
}
|
||||
for (migration in migrations) {
|
||||
helper.runMigrationsAndValidate(
|
||||
TEST_DB,
|
||||
migration.endVersion,
|
||||
true,
|
||||
migration
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TEST_DB = "test-db"
|
||||
|
||||
val migrations = arrayOf(
|
||||
Migration1To2(),
|
||||
Migration2To3(),
|
||||
Migration3To4(),
|
||||
Migration4To5(),
|
||||
Migration5To6(),
|
||||
Migration6To7(),
|
||||
Migration7To8(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:tint="#E6000A">
|
||||
<group android:scaleX="0.40188664"
|
||||
android:scaleY="0.40188664"
|
||||
android:translateX="32.90095"
|
||||
android:translateY="18.7272">
|
||||
<group android:translateY="139.39206">
|
||||
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
|
||||
android:fillColor="#E6000A"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
3
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Kotatsu Dev</string>
|
||||
</resources>
|
||||
@@ -21,9 +21,11 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<activity android:name=".ui.main.MainActivity">
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -32,57 +34,84 @@
|
||||
android:name="android.app.default_searchable"
|
||||
android:value=".ui.search.SearchActivity" />
|
||||
</activity>
|
||||
<activity android:name=".ui.details.MangaDetailsActivity">
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.reader.ReaderActivity" />
|
||||
<activity
|
||||
android:name=".ui.search.SearchActivity"
|
||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="${applicationId}.action.READ_MANGA" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name=".ui.settings.SettingsActivity"
|
||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||
android:label="@string/settings" />
|
||||
<activity
|
||||
android:name=".ui.reader.SimpleSettingsActivity"
|
||||
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.browser.BrowserActivity" />
|
||||
<activity
|
||||
android:name=".ui.utils.CrashActivity"
|
||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
||||
android:label="@string/error_occurred"
|
||||
android:theme="@android:style/Theme.DeviceDefault.Dialog"
|
||||
android:theme="@android:style/Theme.DeviceDefault"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
|
||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
||||
android:label="@string/favourites_categories"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name=".ui.widget.shelf.ShelfConfigActivity"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/manga_shelf">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".settings.protect.ProtectSetupActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
||||
android:label="@string/downloads" />
|
||||
|
||||
<service
|
||||
android:name=".ui.download.DownloadService"
|
||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".ui.settings.AppUpdateService" />
|
||||
<service
|
||||
android:name=".ui.widget.shelf.ShelfWidgetService"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".ui.widget.recent.RecentWidgetService"
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<provider
|
||||
android:name=".ui.search.MangaSuggestionsProvider"
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
||||
android:exported="false" />
|
||||
<provider
|
||||
@@ -96,7 +125,8 @@
|
||||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name=".ui.widget.shelf.ShelfWidgetProvider"
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/manga_shelf">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -106,7 +136,8 @@
|
||||
android:resource="@xml/widget_shelf" />
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".ui.widget.recent.RecentWidgetProvider"
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/recent_manga">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -116,6 +147,13 @@
|
||||
android:resource="@xml/widget_recent" />
|
||||
</receiver>
|
||||
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,128 +1,90 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.room.Room
|
||||
import coil.Coil
|
||||
import coil.ComponentRegistry
|
||||
import coil.ImageLoaderBuilder
|
||||
import coil.util.CoilUtils
|
||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
import org.koitharu.kotatsu.core.local.CbzFetcher
|
||||
import org.koitharu.kotatsu.core.local.PagesCache
|
||||
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
|
||||
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
|
||||
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
|
||||
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.core.db.databaseModule
|
||||
import org.koitharu.kotatsu.core.github.githubModule
|
||||
import org.koitharu.kotatsu.core.network.networkModule
|
||||
import org.koitharu.kotatsu.core.parser.parserModule
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
|
||||
import org.koitharu.kotatsu.domain.history.HistoryRepository
|
||||
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
|
||||
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
||||
import org.koitharu.kotatsu.core.ui.uiModule
|
||||
import org.koitharu.kotatsu.details.detailsModule
|
||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
||||
import org.koitharu.kotatsu.history.historyModule
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.localModule
|
||||
import org.koitharu.kotatsu.main.mainModule
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.reader.readerModule
|
||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||
import org.koitharu.kotatsu.search.searchModule
|
||||
import org.koitharu.kotatsu.settings.settingsModule
|
||||
import org.koitharu.kotatsu.tracker.trackerModule
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||
|
||||
class KotatsuApp : Application() {
|
||||
|
||||
private val cookieJar by lazy {
|
||||
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
|
||||
}
|
||||
|
||||
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ChuckerCollector(applicationContext)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initKoin()
|
||||
initCoil()
|
||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||
if (BuildConfig.DEBUG) {
|
||||
initErrorHandler()
|
||||
enableStrictMode()
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
|
||||
initKoin()
|
||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||
FavouritesRepository.subscribe(widgetUpdater)
|
||||
HistoryRepository.subscribe(widgetUpdater)
|
||||
widgetUpdater.subscribeToFavourites(get())
|
||||
widgetUpdater.subscribeToHistory(get())
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(applicationContext)
|
||||
androidContext(this@KotatsuApp)
|
||||
modules(
|
||||
module {
|
||||
factory {
|
||||
okHttp()
|
||||
.cache(CacheUtils.createHttpCache(applicationContext))
|
||||
.build()
|
||||
}
|
||||
single {
|
||||
mangaDb().build()
|
||||
}
|
||||
single {
|
||||
MangaLoaderContext()
|
||||
}
|
||||
factory {
|
||||
AppSettings(applicationContext)
|
||||
}
|
||||
single {
|
||||
PagesCache(applicationContext)
|
||||
}
|
||||
}
|
||||
networkModule,
|
||||
databaseModule,
|
||||
githubModule,
|
||||
uiModule,
|
||||
parserModule,
|
||||
mainModule,
|
||||
searchModule,
|
||||
localModule,
|
||||
favouritesModule,
|
||||
historyModule,
|
||||
remoteListModule,
|
||||
detailsModule,
|
||||
trackerModule,
|
||||
settingsModule,
|
||||
readerModule,
|
||||
appWidgetModule
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCoil() {
|
||||
Coil.setImageLoader(
|
||||
ImageLoaderBuilder(applicationContext)
|
||||
.okHttpClient(
|
||||
okHttp()
|
||||
.cache(CoilUtils.createDefaultCache(applicationContext))
|
||||
.build()
|
||||
).componentRegistry(
|
||||
ComponentRegistry.Builder()
|
||||
.add(CbzFetcher())
|
||||
.build()
|
||||
)
|
||||
private fun enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun initErrorHandler() {
|
||||
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
chuckerCollector.onError("CRASH", e)
|
||||
exceptionHandler?.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun okHttp() = OkHttpClient.Builder().apply {
|
||||
connectTimeout(20, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(20, TimeUnit.SECONDS)
|
||||
cookieJar(cookieJar)
|
||||
addInterceptor(UserAgentInterceptor)
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
|
||||
}
|
||||
}
|
||||
|
||||
private fun mangaDb() = Room.databaseBuilder(
|
||||
applicationContext,
|
||||
MangaDatabase::class.java,
|
||||
"kotatsu-db"
|
||||
).addMigrations(Migration1To2, Migration2To3, Migration3To4)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.koitharu.kotatsu.domain
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
@@ -10,9 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
|
||||
class MangaDataRepository : KoinComponent {
|
||||
|
||||
private val db: MangaDatabase by inject()
|
||||
class MangaDataRepository(private val db: MangaDatabase) {
|
||||
|
||||
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
@@ -36,6 +32,12 @@ class MangaDataRepository : KoinComponent {
|
||||
return db.mangaDao.find(mangaId)?.toManga()
|
||||
}
|
||||
|
||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||
intent.manga != null -> intent.manga
|
||||
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
|
||||
else -> null // TODO resolve uri
|
||||
}
|
||||
|
||||
suspend fun storeManga(manga: Manga) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.withTransaction {
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
|
||||
class MangaIntent(
|
||||
val manga: Manga?,
|
||||
val mangaId: Long,
|
||||
val uri: Uri?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(intent: Intent?) = MangaIntent(
|
||||
manga = intent?.getParcelableExtra(KEY_MANGA),
|
||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = intent?.data
|
||||
)
|
||||
|
||||
fun from(args: Bundle?) = MangaIntent(
|
||||
manga = args?.getParcelable(KEY_MANGA),
|
||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||
uri = null
|
||||
)
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_ID = "id"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import okhttp3.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
|
||||
open class MangaLoaderContext(
|
||||
private val okHttp: OkHttpClient,
|
||||
val cookieJar: CookieJar
|
||||
) : KoinComponent {
|
||||
|
||||
suspend fun httpGet(url: String, headers: Headers? = null): Response {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
if (headers != null) {
|
||||
request.headers(headers)
|
||||
}
|
||||
return okHttp.newCall(request.build()).await()
|
||||
}
|
||||
|
||||
suspend fun httpPost(
|
||||
url: String,
|
||||
form: Map<String, String>
|
||||
): Response {
|
||||
val body = FormBody.Builder()
|
||||
form.forEach { (k, v) ->
|
||||
body.addEncoded(k, v)
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
.url(url)
|
||||
return okHttp.newCall(request.build()).await()
|
||||
}
|
||||
|
||||
suspend fun httpPost(
|
||||
url: String,
|
||||
payload: String
|
||||
): Response {
|
||||
val body = FormBody.Builder()
|
||||
payload.split('&').forEach {
|
||||
val pos = it.indexOf('=')
|
||||
if (pos != -1) {
|
||||
val k = it.substring(0, pos)
|
||||
val v = it.substring(pos + 1)
|
||||
body.addEncoded(k, v)
|
||||
}
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.post(body.build())
|
||||
.url(url)
|
||||
return okHttp.newCall(request.build()).await()
|
||||
}
|
||||
|
||||
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val SCHEME_HTTP = "http"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
object MangaProviderFactory {
|
||||
|
||||
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
|
||||
val list = MangaSource.values().toList() - MangaSource.LOCAL
|
||||
val order = settings.sourcesOrder
|
||||
val hidden = settings.hiddenSources
|
||||
val sorted = list.sortedBy { x ->
|
||||
val e = order.indexOf(x.ordinal)
|
||||
if (e == -1) order.size + x.ordinal else e
|
||||
}
|
||||
return if (includeHidden) {
|
||||
sorted
|
||||
} else {
|
||||
sorted.filterNot { x ->
|
||||
x.name in hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.koitharu.kotatsu.base.domain
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Size
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.utils.CacheUtils
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
object MangaUtils : KoinComponent {
|
||||
|
||||
/**
|
||||
* Automatic determine type of manga by page size
|
||||
* @return ReaderMode.WEBTOON if page is wide
|
||||
*/
|
||||
@WorkerThread
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
|
||||
try {
|
||||
val page = pages.medianOrNull() ?: return null
|
||||
val url = page.source.repository.getPageUrl(page)
|
||||
val uri = Uri.parse(url)
|
||||
val size = if (uri.scheme == "cbz") {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
}
|
||||
} else {
|
||||
val client = get<OkHttpClient>()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header(CommonHeaders.REFERER, page.referer)
|
||||
.cacheControl(CacheUtils.CONTROL_DISABLED)
|
||||
.build()
|
||||
client.newCall(request).await().use {
|
||||
getBitmapSize(it.body?.byteStream())
|
||||
}
|
||||
}
|
||||
return size.width * 2 < size.height
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBitmapSize(input: InputStream?): Size {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeStream(input, null, options)
|
||||
val imageHeight: Int = options.outHeight
|
||||
val imageWidth: Int = options.outWidth
|
||||
check(imageHeight > 0 && imageWidth > 0)
|
||||
return Size(imageWidth, imageHeight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||
|
||||
private var viewBinding: B? = null
|
||||
|
||||
protected val binding: B
|
||||
get() = checkNotNull(viewBinding)
|
||||
|
||||
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
|
||||
val binding = onInflateView(inflater, null)
|
||||
viewBinding = binding
|
||||
return AlertDialog.Builder(requireContext(), theme)
|
||||
.setView(binding.root)
|
||||
.also(::onBuildDialog)
|
||||
.create()
|
||||
}
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = viewBinding?.root
|
||||
|
||||
@CallSuper
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
|
||||
|
||||
protected fun bindingOrNull(): B? = viewBinding
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
}
|
||||
132
app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.*
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
|
||||
|
||||
protected lateinit var binding: B
|
||||
private set
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(this, supportFragmentManager)
|
||||
}
|
||||
|
||||
private var lastInsets: Insets = Insets.NONE
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (get<AppSettings>().isAmoledTheme) {
|
||||
setTheme(R.style.AppTheme_AMOLED)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
}
|
||||
|
||||
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||
override fun setContentView(layoutResID: Int) {
|
||||
super.setContentView(layoutResID)
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||
override fun setContentView(view: View?) {
|
||||
super.setContentView(view)
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
protected fun setContentView(binding: B) {
|
||||
this.binding = binding
|
||||
super.setContentView(binding.root)
|
||||
val toolbar = (binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)
|
||||
toolbar?.let(this::setSupportActionBar)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
|
||||
|
||||
val toolbarParams = (binding.root.findViewById<View>(R.id.toolbar_card) ?: toolbar)
|
||||
?.layoutParams as? AppBarLayout.LayoutParams
|
||||
if (toolbarParams != null) {
|
||||
if (get<AppSettings>().isToolbarHideWhenScrolling) {
|
||||
toolbarParams.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP
|
||||
} else {
|
||||
toolbarParams.scrollFlags = SCROLL_FLAG_NO_SCROLL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val baseInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
val newInsets = Insets.max(baseInsets, imeInsets)
|
||||
if (newInsets != lastInsets) {
|
||||
onWindowInsetsChanged(newInsets)
|
||||
lastInsets = newInsets
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||
ActivityCompat.recreate(this)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
protected abstract fun onWindowInsetsChanged(insets: Insets)
|
||||
|
||||
private fun setupToolbar() {
|
||||
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
|
||||
}
|
||||
|
||||
protected fun isDarkAmoledTheme(): Boolean {
|
||||
val uiMode = resources.configuration.uiMode
|
||||
val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
return isNight && get<AppSettings>().isAmoledTheme
|
||||
}
|
||||
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
super.onSupportActionModeStarted(mode)
|
||||
val insets = ViewCompat.getRootWindowInsets(binding.root)
|
||||
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
|
||||
view?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if ( // https://issuetracker.google.com/issues/139738913
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
||||
isTaskRoot &&
|
||||
supportFragmentManager.backStackEntryCount == 0
|
||||
) {
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
abstract class BaseBottomSheet<B : ViewBinding> :
|
||||
BottomSheetDialogFragment() {
|
||||
|
||||
private var viewBinding: B? = null
|
||||
|
||||
protected val binding: B
|
||||
get() = checkNotNull(viewBinding)
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = onInflateView(inflater, container)
|
||||
viewBinding = binding
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return if (resources.getBoolean(R.bool.is_tablet)) {
|
||||
AppCompatDialog(context, theme)
|
||||
} else super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
|
||||
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
|
||||
|
||||
private var viewBinding: B? = null
|
||||
|
||||
protected val binding: B
|
||||
get() = checkNotNull(viewBinding)
|
||||
|
||||
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
|
||||
}
|
||||
|
||||
private var lastInsets: Insets = Insets.NONE
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val binding = onInflateView(inflater, container)
|
||||
viewBinding = binding
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
lastInsets = Insets.NONE
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
open fun getTitle(): CharSequence? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
getTitle()?.let {
|
||||
activity?.title = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
if (newInsets != lastInsets) {
|
||||
onWindowInsetsChanged(newInsets)
|
||||
lastInsets = newInsets
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
protected fun bindingOrNull() = viewBinding
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
|
||||
protected abstract fun onWindowInsetsChanged(insets: Insets)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
|
||||
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
|
||||
View.OnSystemUiVisibilityChangeListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
with(window) {
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
||||
}
|
||||
showSystemUI()
|
||||
}
|
||||
|
||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
||||
}
|
||||
|
||||
// TODO WindowInsetsControllerCompat works incorrect
|
||||
protected fun hideSystemUI() {
|
||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
||||
}
|
||||
|
||||
protected fun showSystemUI() {
|
||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
||||
}
|
||||
|
||||
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private companion object {
|
||||
|
||||
const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
|
||||
const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
|
||||
|
||||
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
listView.clipToPadding = false
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(titleId)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
listView.updatePadding(
|
||||
left = systemBars.left,
|
||||
right = systemBars.right,
|
||||
bottom = systemBars.bottom
|
||||
)
|
||||
return insets
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import androidx.lifecycle.LifecycleService
|
||||
|
||||
abstract class BaseService : LifecycleService()
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.koitharu.kotatsu.base.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
val onError = SingleLiveEvent<Throwable>()
|
||||
val isLoading = MutableLiveData(false)
|
||||
|
||||
protected fun launchJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||
|
||||
protected fun launchLoadingJob(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||
isLoading.postValue(true)
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
isLoading.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||
if (BuildConfig.DEBUG) {
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
if (throwable !is CancellationException) {
|
||||
onError.postCall(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.koitharu.kotatsu.ui.common.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
|
||||
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
|
||||
DialogInterface by delegate {
|
||||
@@ -17,13 +15,10 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
||||
|
||||
class Builder(context: Context) {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.dialog_checkbox, null, false)
|
||||
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
|
||||
private val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setView(binding.root)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
delegate.setTitle(titleResId)
|
||||
@@ -46,12 +41,12 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
||||
}
|
||||
|
||||
fun setCheckBoxText(@StringRes textId: Int): Builder {
|
||||
checkBox.setText(textId)
|
||||
binding.checkbox.setText(textId)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setCheckBoxChecked(isChecked: Boolean): Builder {
|
||||
checkBox.isChecked = isChecked
|
||||
binding.checkbox.isChecked = isChecked
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -65,7 +60,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
||||
listener: (DialogInterface, Boolean) -> Unit
|
||||
): Builder {
|
||||
delegate.setPositiveButton(textId) { dialog, _ ->
|
||||
listener(dialog, checkBox.isChecked)
|
||||
listener(dialog, binding.checkbox.isChecked)
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
@@ -7,9 +7,10 @@ import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.item_storage.view.*
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.inflate
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
@@ -64,8 +65,9 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: parent.inflate(R.layout.item_storage)
|
||||
val item = volumes[position]
|
||||
view.textView_title.text = item.second
|
||||
view.textView_subtitle.text = item.first.path
|
||||
val binding = ItemStorageBinding.bind(view)
|
||||
binding.textViewTitle.text = item.second
|
||||
binding.textViewSubtitle.text = item.first.path
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -77,14 +79,13 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
||||
|
||||
}
|
||||
|
||||
interface OnStorageSelectListener {
|
||||
fun interface OnStorageSelectListener {
|
||||
|
||||
fun onStorageSelected(file: File)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
|
||||
return LocalMangaRepository.getAvailableStorageDirs(context).map {
|
||||
it to it.getStorageName(context)
|
||||
@@ -1,27 +1,26 @@
|
||||
package org.koitharu.kotatsu.ui.common.dialog
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.android.synthetic.main.dialog_input.view.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.databinding.DialogInputBinding
|
||||
|
||||
class TextInputDialog private constructor(private val delegate: AlertDialog) :
|
||||
DialogInterface by delegate {
|
||||
class TextInputDialog private constructor(
|
||||
private val delegate: AlertDialog
|
||||
) : DialogInterface by delegate {
|
||||
|
||||
fun show() = delegate.show()
|
||||
|
||||
class Builder(context: Context) {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private val view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null, false)
|
||||
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
private val delegate = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setView(binding.root)
|
||||
|
||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||
delegate.setTitle(titleResId)
|
||||
@@ -34,44 +33,55 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
|
||||
}
|
||||
|
||||
fun setHint(@StringRes hintResId: Int): Builder {
|
||||
view.inputLayout.hint = view.context.getString(hintResId)
|
||||
binding.inputLayout.hint = binding.root.context.getString(hintResId)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
|
||||
with(view.inputLayout) {
|
||||
with(binding.inputLayout) {
|
||||
counterMaxLength = maxLength
|
||||
isCounterEnabled = maxLength > 0
|
||||
}
|
||||
if (strict && maxLength > 0) {
|
||||
view.inputEdit.filters += InputFilter.LengthFilter(maxLength)
|
||||
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun setInputType(inputType: Int): Builder {
|
||||
view.inputEdit.inputType = inputType
|
||||
binding.inputEdit.inputType = inputType
|
||||
return this
|
||||
}
|
||||
|
||||
fun setText(text: String): Builder {
|
||||
view.inputEdit.setText(text)
|
||||
view.inputEdit.setSelection(text.length)
|
||||
binding.inputEdit.setText(text)
|
||||
binding.inputEdit.setSelection(text.length)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder {
|
||||
fun setPositiveButton(
|
||||
@StringRes textId: Int,
|
||||
listener: (DialogInterface, String) -> Unit
|
||||
): Builder {
|
||||
delegate.setPositiveButton(textId) { dialog, _ ->
|
||||
listener(dialog, view.inputEdit.text?.toString().orEmpty())
|
||||
listener(dialog, binding.inputEdit.text.toString().orEmpty())
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun setNegativeButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener? = null): Builder {
|
||||
fun setNegativeButton(
|
||||
@StringRes textId: Int,
|
||||
listener: DialogInterface.OnClickListener? = null
|
||||
): Builder {
|
||||
delegate.setNegativeButton(textId, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder {
|
||||
delegate.setOnCancelListener(listener)
|
||||
return this
|
||||
}
|
||||
|
||||
fun create() =
|
||||
TextInputDialog(delegate.create())
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@Deprecated("")
|
||||
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
|
||||
RecyclerView.ViewHolder(binding.root), KoinComponent {
|
||||
|
||||
var boundData: T? = null
|
||||
private set
|
||||
|
||||
val context get() = itemView.context!!
|
||||
|
||||
fun bind(data: T, extra: E) {
|
||||
boundData = data
|
||||
onBind(data, extra)
|
||||
}
|
||||
|
||||
fun requireData(): T {
|
||||
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
|
||||
}
|
||||
|
||||
open fun onRecycled() = Unit
|
||||
|
||||
abstract fun onBind(data: T, extra: E)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.list
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -12,13 +12,15 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
|
||||
return
|
||||
}
|
||||
if (firstVisibleItemPosition <= offsetTop) {
|
||||
onScrolledToStart(recyclerView)
|
||||
return
|
||||
}
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom && firstVisibleItemPosition >= 0) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
|
||||
onScrolledToEnd(recyclerView)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import android.view.View
|
||||
|
||||
interface OnListItemClickListener<I> {
|
||||
|
||||
fun onItemClick(item: I, view: View)
|
||||
|
||||
fun onItemLongClick(item: I, view: View) = false
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
||||
BoundsScrollListener(0, offset) {
|
||||
|
||||
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
|
||||
|
||||
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||
callback.onScrolledToEnd()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onScrolledToEnd()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.list.decor
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.list.decor
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
@@ -37,7 +37,7 @@ class SectionItemDecoration(
|
||||
|
||||
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.onDrawOver(c, parent, state)
|
||||
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_header).also {
|
||||
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_filter_header).also {
|
||||
headerView = it
|
||||
}
|
||||
fixLayoutSize(textView, parent)
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.base.ui.list.decor
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.annotation.Px
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
outRect.set(spacing, spacing, spacing, spacing)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isGone
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import java.lang.reflect.Field
|
||||
|
||||
class AnimatedToolbar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.toolbarStyle,
|
||||
) : MaterialToolbar(context, attrs, defStyleAttr) {
|
||||
|
||||
private var navButtonView: View? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
runCatching {
|
||||
field = navButtonViewField?.get(this) as? View
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
override fun setNavigationIcon(icon: Drawable?) {
|
||||
super.setNavigationIcon(icon)
|
||||
navButtonView?.isGone = (icon == null)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val navButtonViewField: Field? = runCatching {
|
||||
Toolbar::class.java.getDeclaredField("mNavButtonView")
|
||||
.also { it.isAccessible = true }
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.widgets
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -54,14 +54,13 @@ class CheckableImageView @JvmOverloads constructor(
|
||||
return state
|
||||
}
|
||||
|
||||
interface OnCheckedChangeListener {
|
||||
fun interface OnCheckedChangeListener {
|
||||
|
||||
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@JvmStatic
|
||||
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class ChipsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle
|
||||
) : ChipGroup(context, attrs, defStyleAttr) {
|
||||
|
||||
private var isLayoutSuppressedCompat = false
|
||||
private var isLayoutCalledOnSuppressed = false
|
||||
private var chipOnClickListener = OnClickListener {
|
||||
onChipClickListener?.onChipClick(it as Chip, it.tag)
|
||||
}
|
||||
private var chipOnCloseListener = OnClickListener {
|
||||
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
|
||||
}
|
||||
var onChipClickListener: OnChipClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
val isChipClickable = value != null
|
||||
children.forEach { it.isClickable = isChipClickable }
|
||||
}
|
||||
var onChipCloseClickListener: OnChipCloseClickListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
val isCloseIconVisible = value != null
|
||||
children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible }
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
if (isLayoutSuppressedCompat) {
|
||||
isLayoutCalledOnSuppressed = true
|
||||
} else {
|
||||
super.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun setChips(items: Collection<ChipModel>) {
|
||||
suppressLayoutCompat(true)
|
||||
try {
|
||||
for ((i, model) in items.withIndex()) {
|
||||
val chip = getChildAt(i) as Chip? ?: addChip()
|
||||
bindChip(chip, model)
|
||||
}
|
||||
if (childCount > items.size) {
|
||||
removeViews(items.size, childCount - items.size)
|
||||
}
|
||||
} finally {
|
||||
suppressLayoutCompat(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindChip(chip: Chip, model: ChipModel) {
|
||||
chip.text = model.title
|
||||
if (model.icon == 0) {
|
||||
chip.isChipIconVisible = false
|
||||
} else {
|
||||
chip.isCheckedIconVisible = true
|
||||
chip.setChipIconResource(model.icon)
|
||||
}
|
||||
chip.isClickable = onChipClickListener != null
|
||||
chip.tag = model.data
|
||||
}
|
||||
|
||||
private fun addChip(): Chip {
|
||||
val chip = Chip(context)
|
||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
|
||||
chip.setChipDrawable(drawable)
|
||||
chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary))
|
||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||
chip.setEnsureMinTouchTargetSize(false)
|
||||
chip.setOnClickListener(chipOnClickListener)
|
||||
addView(chip)
|
||||
return chip
|
||||
}
|
||||
|
||||
private fun suppressLayoutCompat(suppress: Boolean) {
|
||||
isLayoutSuppressedCompat = suppress
|
||||
if (!suppress) {
|
||||
if (isLayoutCalledOnSuppressed) {
|
||||
requestLayout()
|
||||
isLayoutCalledOnSuppressed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChipModel(
|
||||
@DrawableRes val icon: Int,
|
||||
val title: CharSequence,
|
||||
val data: Any? = null
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ChipModel
|
||||
|
||||
if (icon != other.icon) return false
|
||||
if (title != other.title) return false
|
||||
if (data != other.data) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = icon
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + data.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun interface OnChipClickListener {
|
||||
|
||||
fun onChipClick(chip: Chip, data: Any?)
|
||||
}
|
||||
|
||||
fun interface OnChipCloseClickListener {
|
||||
|
||||
fun onChipCloseClick(chip: Chip, data: Any?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import org.koitharu.kotatsu.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class CoverImageView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
|
||||
) : AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var orientation: Int = HORIZONTAL
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
|
||||
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val desiredWidth: Int
|
||||
val desiredHeight: Int
|
||||
if (orientation == VERTICAL) {
|
||||
desiredHeight = measuredHeight
|
||||
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
|
||||
} else {
|
||||
desiredWidth = measuredWidth
|
||||
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
|
||||
}
|
||||
setMeasuredDimension(desiredWidth, desiredHeight)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val VERTICAL = LinearLayout.VERTICAL
|
||||
const val HORIZONTAL = LinearLayout.HORIZONTAL
|
||||
|
||||
private const val ASPECT_RATIO_HEIGHT = 18f
|
||||
private const val ASPECT_RATIO_WIDTH = 13f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2018 Google LLC
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.postDelayed
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
/**
|
||||
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
|
||||
*
|
||||
* Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
|
||||
*
|
||||
* https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
|
||||
*/
|
||||
class FadingSnackbar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val message: TextView
|
||||
private val action: Button
|
||||
|
||||
init {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
|
||||
message = view.findViewById(R.id.snackbar_text)
|
||||
action = view.findViewById(R.id.snackbar_action)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
if (visibility == VISIBLE && alpha == 1f) {
|
||||
animate()
|
||||
.alpha(0f)
|
||||
.withEndAction { visibility = GONE }
|
||||
.duration = EXIT_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
fun show(
|
||||
messageText: CharSequence? = null,
|
||||
@StringRes actionId: Int? = null,
|
||||
longDuration: Boolean = true,
|
||||
actionClick: () -> Unit = { dismiss() },
|
||||
dismissListener: () -> Unit = { }
|
||||
) {
|
||||
message.text = messageText
|
||||
if (actionId != null) {
|
||||
action.run {
|
||||
visibility = VISIBLE
|
||||
text = context.getString(actionId)
|
||||
setOnClickListener {
|
||||
actionClick()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
action.visibility = GONE
|
||||
}
|
||||
alpha = 0f
|
||||
visibility = VISIBLE
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.duration = ENTER_DURATION
|
||||
val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
|
||||
postDelayed(showDuration) {
|
||||
dismiss()
|
||||
dismissListener()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENTER_DURATION = 300L
|
||||
private const val EXIT_DURATION = 200L
|
||||
private const val SHORT_DURATION = 1_500L
|
||||
private const val LONG_DURATION = 2_750L
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.common.widgets
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.browser
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
@@ -8,47 +8,53 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.activity_browser.*
|
||||
import androidx.core.view.updatePadding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class BrowserActivity : BaseActivity(), BrowserCallback {
|
||||
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_browser)
|
||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(R.drawable.ic_cross)
|
||||
}
|
||||
with(webView.settings) {
|
||||
with(binding.webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
}
|
||||
webView.webViewClient = BrowserClient(this)
|
||||
binding.webView.webViewClient = BrowserClient(this)
|
||||
val url = intent?.dataString
|
||||
if (url.isNullOrEmpty()) {
|
||||
finish()
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
webView.loadUrl(url)
|
||||
onTitleChanged(
|
||||
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
||||
url
|
||||
)
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_browser, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
webView.stopLoading()
|
||||
finish()
|
||||
binding.webView.stopLoading()
|
||||
finishAfterTransition()
|
||||
true
|
||||
}
|
||||
R.id.action_browser -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(webView.url)
|
||||
intent.data = Uri.parse(binding.webView.url)
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, item.title))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
@@ -59,25 +65,25 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
if (binding.webView.canGoBack()) {
|
||||
binding.webView.goBack()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
webView.onPause()
|
||||
binding.webView.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
webView.onResume()
|
||||
binding.webView.onResume()
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
progressBar.isVisible = isLoading
|
||||
binding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||
@@ -85,10 +91,27 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
|
||||
supportActionBar?.subtitle = subtitle
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.appbar.updatePadding(
|
||||
top = insets.top,
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newIntent(context: Context, url: String) = Intent(context, BrowserActivity::class.java)
|
||||
.setData(Uri.parse(url))
|
||||
private const val EXTRA_TITLE = "title"
|
||||
|
||||
fun newIntent(context: Context, url: String, title: String?): Intent {
|
||||
return Intent(context, BrowserActivity::class.java)
|
||||
.setData(Uri.parse(url))
|
||||
.putExtra(EXTRA_TITLE, title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.browser
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
interface BrowserCallback {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.core.network.WebViewClientCompat
|
||||
|
||||
class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat(), KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>(mode = LazyThreadSafetyMode.SYNCHRONIZED)
|
||||
|
||||
override fun onPageFinished(webView: WebView, url: String) {
|
||||
super.onPageFinished(webView, url)
|
||||
callback.onLoadingStateChanged(isLoading = false)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
callback.onLoadingStateChanged(isLoading = true)
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView, url: String?) {
|
||||
super.onPageCommitVisible(view, url)
|
||||
callback.onTitleChanged(view.title.orEmpty(), url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
interface CloudFlareCallback {
|
||||
|
||||
fun onPageLoaded()
|
||||
|
||||
fun onCheckPassed()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.WebViewClientCompat
|
||||
|
||||
class CloudFlareClient(
|
||||
private val cookieJar: AndroidCookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
private val targetUrl: String
|
||||
) : WebViewClientCompat() {
|
||||
|
||||
private val oldClearance = getCookieValue(CF_CLEARANCE)
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
checkClearance()
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||
super.onPageCommitVisible(view, url)
|
||||
callback.onPageLoaded()
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
callback.onPageLoaded()
|
||||
}
|
||||
|
||||
private fun checkClearance() {
|
||||
val clearance = getCookieValue(CF_CLEARANCE)
|
||||
if (clearance != null && clearance != oldClearance) {
|
||||
callback.onCheckPassed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCookieValue(name: String): String? {
|
||||
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
||||
.find { it.name == name }?.value
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val CF_CLEARANCE = "cf_clearance"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||
|
||||
private val url by stringArgument(ARG_URL)
|
||||
private val pendingResult = Bundle(1)
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding.webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
userAgentString = UserAgentInterceptor.userAgent
|
||||
}
|
||||
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
||||
if (url.isNullOrEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
binding.webView.loadUrl(url.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.webView.stopLoading()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setNegativeButton(android.R.string.cancel, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.webView.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
binding.webView.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
setFragmentResult(TAG, pendingResult)
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
override fun onPageLoaded() {
|
||||
bindingOrNull()?.progressBar?.isInvisible = true
|
||||
}
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareDialog"
|
||||
const val EXTRA_RESULT = "result"
|
||||
private const val ARG_URL = "url"
|
||||
|
||||
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
|
||||
putString(ARG_URL, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.MutableZipFile
|
||||
import org.koitharu.kotatsu.utils.ext.format
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class BackupArchive(file: File) : MutableZipFile(file) {
|
||||
|
||||
init {
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun put(entry: BackupEntry) {
|
||||
put(entry.name, entry.data.toString(2))
|
||||
}
|
||||
|
||||
suspend fun getEntry(name: String): BackupEntry {
|
||||
val json = withContext(Dispatchers.Default) {
|
||||
JSONArray(getContent(name))
|
||||
}
|
||||
return BackupEntry(name, json)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DIR_BACKUPS = "backups"
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
|
||||
val dir = context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
}
|
||||
dir.mkdirs()
|
||||
val filename = buildString {
|
||||
append(context.getString(R.string.app_name).toLowerCase(Locale.ROOT))
|
||||
append('_')
|
||||
append(Date().format("ddMMyyyy"))
|
||||
append(".bak")
|
||||
}
|
||||
BackupArchive(File(dir, filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import org.json.JSONArray
|
||||
|
||||
class BackupEntry(
|
||||
val name: String,
|
||||
val data: JSONArray
|
||||
) {
|
||||
|
||||
companion object Names {
|
||||
|
||||
const val INDEX = "index"
|
||||
const val HISTORY = "history"
|
||||
const val CATEGORIES = "categories"
|
||||
const val FAVOURITES = "favourites"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
|
||||
class BackupRepository(private val db: MangaDatabase) {
|
||||
|
||||
suspend fun dumpHistory(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
||||
while (true) {
|
||||
val history = db.historyDao.findAll(offset, PAGE_SIZE)
|
||||
if (history.isEmpty()) {
|
||||
break
|
||||
}
|
||||
offset += history.size
|
||||
for (item in history) {
|
||||
val manga = item.manga.toJson()
|
||||
val tags = JSONArray()
|
||||
item.tags.forEach { tags.put(it.toJson()) }
|
||||
manga.put("tags", tags)
|
||||
val json = item.history.toJson()
|
||||
json.put("manga", manga)
|
||||
entry.data.put(json)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun dumpCategories(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
||||
val categories = db.favouriteCategoriesDao.findAll()
|
||||
for (item in categories) {
|
||||
entry.data.put(item.toJson())
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun dumpFavourites(): BackupEntry {
|
||||
var offset = 0
|
||||
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
||||
while (true) {
|
||||
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
|
||||
if (favourites.isEmpty()) {
|
||||
break
|
||||
}
|
||||
offset += favourites.size
|
||||
for (item in favourites) {
|
||||
val manga = item.manga.toJson()
|
||||
val tags = JSONArray()
|
||||
item.tags.forEach { tags.put(it.toJson()) }
|
||||
manga.put("tags", tags)
|
||||
val json = item.favourite.toJson()
|
||||
json.put("manga", manga)
|
||||
entry.data.put(json)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun createIndex(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
||||
val json = JSONObject()
|
||||
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||
json.put("created_at", System.currentTimeMillis())
|
||||
entry.data.put(json)
|
||||
return entry
|
||||
}
|
||||
|
||||
private fun MangaEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("id", id)
|
||||
jo.put("title", title)
|
||||
jo.put("alt_title", altTitle)
|
||||
jo.put("url", url)
|
||||
jo.put("public_url", publicUrl)
|
||||
jo.put("rating", rating)
|
||||
jo.put("nsfw", isNsfw)
|
||||
jo.put("cover_url", coverUrl)
|
||||
jo.put("large_cover_url", largeCoverUrl)
|
||||
jo.put("state", state)
|
||||
jo.put("author", author)
|
||||
jo.put("source", source)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun TagEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("id", id)
|
||||
jo.put("title", title)
|
||||
jo.put("key", key)
|
||||
jo.put("source", source)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun HistoryEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("manga_id", mangaId)
|
||||
jo.put("created_at", createdAt)
|
||||
jo.put("updated_at", updatedAt)
|
||||
jo.put("chapter_id", chapterId)
|
||||
jo.put("page", page)
|
||||
jo.put("scroll", scroll)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun FavouriteCategoryEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("category_id", categoryId)
|
||||
jo.put("created_at", createdAt)
|
||||
jo.put("sort_key", sortKey)
|
||||
jo.put("title", title)
|
||||
jo.put("order", order)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun FavouriteEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("manga_id", mangaId)
|
||||
jo.put("category_id", categoryId)
|
||||
jo.put("created_at", createdAt)
|
||||
return jo
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val PAGE_SIZE = 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
class CompositeResult {
|
||||
|
||||
private var successCount: Int = 0
|
||||
private val errors = ArrayList<Throwable?>()
|
||||
|
||||
val size: Int
|
||||
get() = successCount + errors.size
|
||||
|
||||
val failures: List<Throwable>
|
||||
get() = errors.filterNotNull()
|
||||
|
||||
val isAllSuccess: Boolean
|
||||
get() = errors.none { it != null }
|
||||
|
||||
val isAllFailed: Boolean
|
||||
get() = successCount == 0 && errors.isNotEmpty()
|
||||
|
||||
operator fun plusAssign(result: Result<*>) {
|
||||
when {
|
||||
result.isSuccess -> successCount++
|
||||
result.isFailure -> errors.add(result.exceptionOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
operator fun plusAssign(other: CompositeResult) {
|
||||
this.successCount += other.successCount
|
||||
this.errors += other.errors
|
||||
}
|
||||
|
||||
operator fun plus(other: CompositeResult): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
result.successCount = this.successCount + other.successCount
|
||||
result.errors.addAll(this.errors)
|
||||
result.errors.addAll(other.errors)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.utils.ext.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.utils.ext.getStringOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.iterator
|
||||
import org.koitharu.kotatsu.utils.ext.map
|
||||
|
||||
class RestoreRepository(private val db: MangaDatabase) {
|
||||
|
||||
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = parseManga(mangaJson)
|
||||
val tags = mangaJson.getJSONArray("tags").map {
|
||||
parseTag(it)
|
||||
}
|
||||
val history = parseHistory(item)
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.historyDao.upsert(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data) {
|
||||
val category = parseCategory(item)
|
||||
result += runCatching {
|
||||
db.favouriteCategoriesDao.upsert(category)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = parseManga(mangaJson)
|
||||
val tags = mangaJson.getJSONArray("tags").map {
|
||||
parseTag(it)
|
||||
}
|
||||
val favourite = parseFavourite(item)
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.favouritesDao.upsert(favourite)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseManga(json: JSONObject) = MangaEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
altTitle = json.getStringOrNull("alt_title"),
|
||||
url = json.getString("url"),
|
||||
publicUrl = json.getStringOrNull("public_url").orEmpty(),
|
||||
rating = json.getDouble("rating").toFloat(),
|
||||
isNsfw = json.getBooleanOrDefault("nsfw", false),
|
||||
coverUrl = json.getString("cover_url"),
|
||||
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
||||
state = json.getStringOrNull("state"),
|
||||
author = json.getStringOrNull("author"),
|
||||
source = json.getString("source")
|
||||
)
|
||||
|
||||
private fun parseTag(json: JSONObject) = TagEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
key = json.getString("key"),
|
||||
source = json.getString("source")
|
||||
)
|
||||
|
||||
private fun parseHistory(json: JSONObject) = HistoryEntity(
|
||||
mangaId = json.getLong("manga_id"),
|
||||
createdAt = json.getLong("created_at"),
|
||||
updatedAt = json.getLong("updated_at"),
|
||||
chapterId = json.getLong("chapter_id"),
|
||||
page = json.getInt("page"),
|
||||
scroll = json.getDouble("scroll").toFloat()
|
||||
)
|
||||
|
||||
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
|
||||
categoryId = json.getInt("category_id"),
|
||||
createdAt = json.getLong("created_at"),
|
||||
sortKey = json.getInt("sort_key"),
|
||||
title = json.getString("title"),
|
||||
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
|
||||
)
|
||||
|
||||
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
|
||||
mangaId = json.getLong("manga_id"),
|
||||
categoryId = json.getLong("category_id"),
|
||||
createdAt = json.getLong("created_at")
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Room
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.db.migrations.*
|
||||
|
||||
val databaseModule
|
||||
get() = module {
|
||||
single {
|
||||
Room.databaseBuilder(
|
||||
androidContext(),
|
||||
MangaDatabase::class.java,
|
||||
"kotatsu-db"
|
||||
).addMigrations(
|
||||
Migration1To2(),
|
||||
Migration2To3(),
|
||||
Migration3To4(),
|
||||
Migration4To5(),
|
||||
Migration5To6(),
|
||||
Migration6To7(),
|
||||
Migration7To8(),
|
||||
Migration8To9(),
|
||||
).addCallback(
|
||||
DatabasePrePopulateCallback(androidContext().resources)
|
||||
).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
|
||||
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)",
|
||||
arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
|
||||
|
||||
@Dao
|
||||
abstract class FavouriteCategoriesDao {
|
||||
|
||||
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy")
|
||||
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||
|
||||
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
|
||||
abstract suspend fun delete(id: Long)
|
||||
|
||||
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
|
||||
abstract suspend fun update(id: Long, title: String)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Dao
|
||||
abstract class FavouritesDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||
abstract suspend fun find(id: Long): FavouriteManga?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun add(favourite: FavouriteEntity)
|
||||
|
||||
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||
abstract suspend fun delete(categoryId: Long, mangaId: Long)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
|
||||
@Dao
|
||||
abstract class HistoryDao {
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
@Query("SELECT * FROM history WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): HistoryEntity?
|
||||
|
||||
@Query("DELETE FROM history")
|
||||
abstract suspend fun clear()
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: HistoryEntity): Long
|
||||
|
||||
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
|
||||
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
|
||||
|
||||
@Query("DELETE FROM history WHERE manga_id = :mangaId")
|
||||
abstract suspend fun delete(mangaId: Long)
|
||||
|
||||
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(entity: HistoryEntity): Boolean {
|
||||
return if (update(entity) == 0) {
|
||||
insert(entity)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,13 +2,21 @@ package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import org.koitharu.kotatsu.core.db.dao.*
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouritesDao
|
||||
import org.koitharu.kotatsu.history.data.HistoryDao
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
|
||||
], version = 4
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
|
||||
], version = 9
|
||||
)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
@@ -25,4 +33,6 @@ abstract class MangaDatabase : RoomDatabase() {
|
||||
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
||||
|
||||
abstract val tracksDao: TracksDao
|
||||
|
||||
abstract val trackLogsDao: TrackLogsDao
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
@@ -13,6 +13,14 @@ abstract class MangaDao {
|
||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): MangaWithTags?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(manga: MangaEntity): Long
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
@@ -1,22 +1,22 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Dao
|
||||
interface TagsDao {
|
||||
abstract class TagsDao {
|
||||
|
||||
@Query("SELECT * FROM tags")
|
||||
suspend fun getAllTags(): List<TagEntity>
|
||||
abstract suspend fun getAllTags(): List<TagEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insert(tag: TagEntity): Long
|
||||
abstract suspend fun insert(tag: TagEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun update(tag: TagEntity): Int
|
||||
abstract suspend fun update(tag: TagEntity): Int
|
||||
|
||||
@Transaction
|
||||
suspend fun upsert(tags: Iterable<TagEntity>) {
|
||||
open suspend fun upsert(tags: Iterable<TagEntity>) {
|
||||
tags.forEach { tag ->
|
||||
if (update(tag) <= 0) {
|
||||
insert(tag)
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga
|
||||
|
||||
@Dao
|
||||
interface TrackLogsDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||
suspend fun findAll(offset: Int, limit: Int): List<TrackLogWithManga>
|
||||
|
||||
@Query("DELETE FROM track_logs")
|
||||
suspend fun clear()
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(entity: TrackLogEntity): Long
|
||||
|
||||
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
|
||||
suspend fun removeAll(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
||||
suspend fun cleanup()
|
||||
|
||||
@Query("SELECT COUNT(*) FROM track_logs")
|
||||
suspend fun count(): Int
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
||||
@@ -25,11 +25,13 @@ abstract class TracksDao {
|
||||
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract suspend fun delete(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
|
||||
abstract suspend fun cleanup()
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(entity: TrackEntity) {
|
||||
if (update(entity) == 0) {
|
||||
insert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,17 +9,19 @@ import org.koitharu.kotatsu.core.model.MangaState
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
|
||||
@Entity(tableName = "manga")
|
||||
data class MangaEntity(
|
||||
class MangaEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
|
||||
@ColumnInfo(name = "alt_title") val altTitle: String?,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
|
||||
@ColumnInfo(name = "public_url") val publicUrl: String,
|
||||
@ColumnInfo(name = "rating") val rating: Float, //normalized value [0..1] or -1
|
||||
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
|
||||
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
|
||||
@ColumnInfo(name = "state") val state: String? = null,
|
||||
@ColumnInfo(name = "author") val author: String? = null,
|
||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
|
||||
@ColumnInfo(name = "state") val state: String?,
|
||||
@ColumnInfo(name = "author") val author: String?,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
@@ -29,7 +31,9 @@ data class MangaEntity(
|
||||
altTitle = this.altTitle,
|
||||
state = this.state?.let { MangaState.valueOf(it) },
|
||||
rating = this.rating,
|
||||
isNsfw = this.isNsfw,
|
||||
url = this.url,
|
||||
publicUrl = this.publicUrl,
|
||||
coverUrl = this.coverUrl,
|
||||
largeCoverUrl = this.largeCoverUrl,
|
||||
author = this.author,
|
||||
@@ -42,11 +46,13 @@ data class MangaEntity(
|
||||
fun from(manga: Manga) = MangaEntity(
|
||||
id = manga.id,
|
||||
url = manga.url,
|
||||
publicUrl = manga.publicUrl,
|
||||
source = manga.source.name,
|
||||
largeCoverUrl = manga.largeCoverUrl,
|
||||
coverUrl = manga.coverUrl,
|
||||
altTitle = manga.altTitle,
|
||||
rating = manga.rating,
|
||||
isNsfw = manga.isNsfw,
|
||||
state = manga.state?.name,
|
||||
title = manga.title,
|
||||
author = manga.author
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.room.PrimaryKey
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class MangaPrefsEntity(
|
||||
class MangaPrefsEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "mode") val mode: Int
|
||||
|
||||
@@ -20,7 +20,7 @@ import androidx.room.ForeignKey
|
||||
)
|
||||
]
|
||||
)
|
||||
data class MangaTagsEntity(
|
||||
class MangaTagsEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
|
||||
)
|
||||
@@ -3,8 +3,9 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
data class MangaWithTags(
|
||||
class MangaWithTags(
|
||||
@Embedded val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
@@ -14,7 +15,7 @@ data class MangaWithTags(
|
||||
val tags: List<TagEntity>
|
||||
) {
|
||||
|
||||
fun toManga() = manga.toManga(tags.map {
|
||||
fun toManga() = manga.toManga(tags.mapToSet {
|
||||
it.toMangaTag()
|
||||
}.toSet())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "suggestions",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
class SuggestionEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "relevance") val relevance: Float,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
)
|
||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
|
||||
@Entity(tableName = "tags")
|
||||
data class TagEntity(
|
||||
class TagEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "tag_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
|
||||
)
|
||||
]
|
||||
)
|
||||
data class TrackEntity (
|
||||
class TrackEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "track_logs", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
class TrackLogEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "id") val id: Long = 0L,
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "chapters") val chapters: String,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import java.util.*
|
||||
|
||||
class TrackLogWithManga(
|
||||
@Embedded val trackLog: TrackLogEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "manga_id"
|
||||
)
|
||||
val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>
|
||||
) {
|
||||
|
||||
fun toTrackingLogItem() = TrackingLogItem(
|
||||
id = trackLog.id,
|
||||
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
||||
manga = manga.toManga(tags.mapToSet { x -> x.toMangaTag() }),
|
||||
createdAt = Date(trackLog.createdAt)
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object Migration1To2 : Migration(1, 2) {
|
||||
class Migration1To2 : Migration(1, 2) {
|
||||
/**
|
||||
* Adding foreign keys
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object Migration2To3 : Migration(2, 3) {
|
||||
class Migration2To3 : Migration(2, 3) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object Migration3To4 : Migration(3, 4) {
|
||||
class Migration3To4 : Migration(3, 4) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration4To5 : Migration(4, 5) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration5To6 : Migration(5, 6) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration6To7 : Migration(6, 7) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration7To8 : Migration(7, 8) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
|
||||
class Migration8To9 : Migration(8, 9) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
|
||||
}
|
||||
}
|
||||