Compare commits
1 Commits
feature/do
...
v0.4-legac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe40ac17e |
@@ -1,21 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
disabled_rules = no-wildcard-imports, no-unused-imports
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_continuation_indent_size = 4
|
||||
ij_xml_attribute_wrap = on_every_item
|
||||
|
||||
[{*.kt,*.kts}]
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
||||
ko_fi: xtimms
|
||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
||||
custom: ["https://money.yandex.ru/to/410012543938752"]
|
||||
|
||||
29
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,29 +0,0 @@
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (https://github.com/KotatsuApp/Kotatsu/releases/latest)
|
||||
- If this is an issue with a parser, that I should be opening an issue in https://github.com/KotatsuApp/kotatsu-parsers
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## Device information
|
||||
* Kotatsu version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
## Steps to reproduce
|
||||
1. First step
|
||||
2. Second step
|
||||
|
||||
## Issue/Request
|
||||
?
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ⚠️ Source issue
|
||||
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
||||
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead
|
||||
66
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -1,66 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug in Kotatsu
|
||||
labels: [bug]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Brief summary
|
||||
description: Please describe, what went wrong
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Please provide a way to reproduce this issue. Screenshots or videos can be very helpful
|
||||
placeholder: |
|
||||
Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Issue here
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
- type: input
|
||||
id: kotatsu-version
|
||||
attributes:
|
||||
label: Kotatsu version
|
||||
description: You can find your Kotatsu version in **Settings → About**.
|
||||
placeholder: |
|
||||
Example: "3.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "12.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "LG Nexus 5X"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||
required: true
|
||||
24
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: ⭐ Feature request
|
||||
description: Suggest a new idea how to improve Kotatsu
|
||||
labels: [feature request]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: How can Kotatsu be improved?
|
||||
placeholder: |
|
||||
Example:
|
||||
"It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
29
.github/workflows/issue_moderator.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Kotatsu version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
}
|
||||
]
|
||||
10
.gitignore
vendored
@@ -3,20 +3,10 @@
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/dictionaries
|
||||
/.idea/modules.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/discord.xml
|
||||
/.idea/compiler.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/kotlinScripting.xml
|
||||
/.idea/kotlinc.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
/.idea/render.experimental.xml
|
||||
/.idea/inspectionProfiles/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
4
.idea/.gitignore
generated
vendored
@@ -1,4 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
/migrations.xml
|
||||
1
.idea/codeStyles/Project.xml
generated
@@ -23,7 +23,6 @@
|
||||
</option>
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="CMake">
|
||||
|
||||
8
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel>
|
||||
<module name="Kotatsu.app" target="1.8" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/dictionaries/admin.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
8
.idea/gradle.xml
generated
@@ -4,16 +4,18 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
287
.idea/icon.svg
generated
@@ -1,287 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:export-ydpi="39.689999"
|
||||
inkscape:export-xdpi="39.689999"
|
||||
inkscape:export-filename="/home/admin/Documents/projects/graphics/k/icon4.png"
|
||||
width="512mm"
|
||||
height="512mm"
|
||||
viewBox="0 0 512 512.00002"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||
sodipodi:docname="icon4.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter1266">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood1256" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite1258" />
|
||||
<feGaussianBlur
|
||||
in="composite1"
|
||||
stdDeviation="3"
|
||||
result="blur"
|
||||
id="feGaussianBlur1260" />
|
||||
<feOffset
|
||||
dx="6"
|
||||
dy="6"
|
||||
result="offset"
|
||||
id="feOffset1262" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite1264" />
|
||||
</filter>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter1059">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood1049" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite1051" />
|
||||
<feGaussianBlur
|
||||
in="composite1"
|
||||
stdDeviation="3"
|
||||
result="blur"
|
||||
id="feGaussianBlur1053" />
|
||||
<feOffset
|
||||
dx="6"
|
||||
dy="6"
|
||||
result="offset"
|
||||
id="feOffset1055" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite1057" />
|
||||
</filter>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter1071">
|
||||
<feFlood
|
||||
flood-opacity="0.498039"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood1061" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite1063" />
|
||||
<feGaussianBlur
|
||||
in="composite1"
|
||||
stdDeviation="3"
|
||||
result="blur"
|
||||
id="feGaussianBlur1065" />
|
||||
<feOffset
|
||||
dx="6"
|
||||
dy="6"
|
||||
result="offset"
|
||||
id="feOffset1067" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite1069" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#0d47a1"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.175"
|
||||
inkscape:cx="-361.03654"
|
||||
inkscape:cy="630.78782"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1600"
|
||||
inkscape:window-height="838"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="20"
|
||||
fit-margin-left="20"
|
||||
fit-margin-right="20"
|
||||
fit-margin-bottom="20" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Слой 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-51.12025,-104.74797)">
|
||||
<g
|
||||
id="g1028"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1030"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1032"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1034"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1036"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1038"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1040"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1042"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1044"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1046"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1048"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1050"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1052"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1054"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<g
|
||||
id="g1056"
|
||||
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||
<path
|
||||
id="path1128"
|
||||
d="m 307.12025,310.74755 c -50.53732,0 -91.66608,44.85688 -91.66608,99.99965 0,55.14277 41.12954,99.99964 91.66608,99.99964 50.53653,0 91.66607,-44.85687 91.66607,-99.99964 0,-55.14277 -41.12875,-99.99965 -91.66607,-99.99965 z m -34.21238,78.72707 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33357 c 4.11951,2.06718 5.77966,7.06403 3.72889,11.18199 z m 58.33338,-24.99991 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52264,-0.27656 -3.72733,-0.8789 l -12.93901,-6.47811 -12.9398,6.47811 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05078,-4.11796 -0.39063,-9.11482 3.72733,-11.182 l 16.66634,-8.33356 c 2.34375,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33356 c 4.11874,2.06718 5.77889,7.06404 3.72811,11.182 z m 54.60606,13.81792 c 4.11795,2.06718 5.7781,7.06403 3.72733,11.18199 -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.11795,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 z"
|
||||
style="fill:#ffffff;stroke-width:0.781247" />
|
||||
<path
|
||||
id="path1130"
|
||||
d="m 415.36283,274.00237 c -3.48202,-6.90544 -6.92029,-13.41714 -10.20934,-19.37102 l -8.26716,-14.47964 c -6.79607,-11.51871 -12.25699,-19.90305 -14.78354,-23.66554 -0.7164,-43.32797 -19.12415,-53.79356 -21.25617,-54.86777 -3.20624,-1.5789 -7.09607,-0.97656 -9.6195,1.56249 -12.25621,12.25621 -20.23118,24.4632 -24.00695,30.89286 h -40.20141 c -3.77577,-6.42888 -11.75153,-18.63665 -24.00695,-30.89286 -2.52265,-2.53905 -6.39685,-3.14139 -9.6195,-1.56249 -2.13202,1.07421 -20.54055,11.5398 -21.25617,54.86777 -2.52655,3.76327 -7.98669,12.14605 -14.78276,23.66476 l -8.27341,14.49214 c -3.28983,5.95701 -6.73044,12.47105 -10.21403,19.3804 l -7.4445,15.32572 c -17.72572,38.05377 -34.30066,85.4286 -34.30066,129.72766 0,69.32085 58.26776,128.42064 60.75838,130.89485 0.91171,0.91172 2.03437,1.61171 3.25546,2.01796 1.07421,0.35859 26.78974,8.757 69.31928,8.757 2.21327,0 4.32967,-0.8789 5.89217,-2.4414 l 5.89216,-5.89216 h 9.76559 l 5.89217,5.89216 c 1.56249,1.5625 3.67811,2.4414 5.89216,2.4414 42.52954,0 68.24507,-8.39841 69.31929,-8.757 1.22109,-0.40703 2.34374,-1.10703 3.25546,-2.01796 2.48905,-2.47421 60.75681,-61.57322 60.75681,-130.89485 0,-44.29906 -16.57494,-91.67389 -34.30066,-129.72766 z M 348.7865,227.41426 c 4.60624,0 4.41171,7.35466 4.41171,11.96089 4.60623,0 12.25464,0.1 12.25464,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19528,0 -16.66634,-7.47028 -16.66634,-16.66634 0,-9.19606 7.47106,-16.66713 16.66634,-16.66713 z m -57.69823,30.14364 c 1.28593,-3.10858 4.32967,-5.14295 7.69841,-5.14295 h 16.66635 c 3.36952,0 6.41248,2.03437 7.69841,5.14295 1.28593,3.10858 0.56953,6.70545 -1.80703,9.082 l -8.33356,8.33356 c -1.62734,1.62734 -3.76014,2.4414 -5.89217,2.4414 -2.13202,0 -4.26404,-0.81406 -5.89216,-2.4414 l -8.33357,-8.33356 c -2.37421,-2.37655 -3.09061,-5.97342 -1.80468,-9.082 z m -25.63428,-30.14364 c 4.60623,0 4.4117,7.35466 4.4117,11.96089 4.60623,0 12.25465,0.1 12.25465,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19606,0 -16.66635,-7.47106 -16.66635,-16.66634 -7.8e-4,-9.19606 7.47029,-16.66713 16.66635,-16.66713 z m 41.66626,299.99893 c -59.7326,0 -108.33321,-52.34357 -108.33321,-116.66599 0,-64.32243 48.60061,-116.66599 108.33321,-116.66599 59.73259,0 108.3332,52.34356 108.3332,116.66599 0,64.32242 -48.60061,116.66599 -108.3332,116.66599 z"
|
||||
style="fill:#ffffff;stroke-width:0.781247;filter:url(#filter1059)"
|
||||
sodipodi:nodetypes="cccccccccccccccsccssccssccsccscsssssssssssccsscsscssssss" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1138"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1140"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1142"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1144"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1146"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1148"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1150"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1152"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1154"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1156"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1158"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1160"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1162"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1164"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
id="g1166"
|
||||
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||
<path
|
||||
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 344.3189,392.83707 c -4.60362,-2.75958 -5.36974,-9.69605 -1.45595,-13.18226 0.54459,-0.48508 5.34567,-3.07035 10.66909,-5.74503 7.5498,-3.79328 10.16725,-4.86303 11.89884,-4.86303 1.73503,0 4.42542,1.10391 12.3172,5.05396 11.72559,5.86898 12.60994,6.68326 12.60994,11.61118 0,3.40408 -0.99553,5.20819 -4.00363,7.25549 -3.08358,2.09867 -5.44113,1.68547 -13.60905,-2.38528 l -7.19926,-3.58796 -7.37198,3.59617 c -8.3911,4.09331 -10.26721,4.39753 -13.8552,2.24676 z"
|
||||
id="path944" />
|
||||
<path
|
||||
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 285.98437,367.98056 c -3.86343,-2.35557 -5.1524,-8.06518 -2.66781,-11.81734 1.64304,-2.48125 20.719,-12.23981 23.92632,-12.23981 1.56364,0 4.61398,1.26582 12.2153,5.06905 8.53551,4.27064 10.3157,5.36752 11.30239,6.96403 1.75651,2.84207 1.95178,5.62136 0.58856,8.37633 -1.52635,3.08463 -3.36973,4.32306 -6.86644,4.61304 -2.68142,0.22236 -3.36743,-0.003 -10.22731,-3.35873 l -7.35311,-3.59707 -7.04119,3.52834 c -7.90523,3.96133 -10.62609,4.44409 -13.87671,2.46216 z"
|
||||
id="path946" />
|
||||
<path
|
||||
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 228.11707,393.18031 c -1.0244,-0.54435 -2.42484,-1.80721 -3.11209,-2.80633 -1.05812,-1.53828 -1.2181,-2.32693 -1.04433,-5.14815 0.29039,-4.71472 1.41139,-5.70783 12.90113,-11.42937 7.71258,-3.84061 9.99443,-4.74971 11.92193,-4.74971 1.94819,0 4.22735,0.92952 12.47354,5.08716 8.66324,4.3679 10.26522,5.3693 11.33052,7.08263 3.53608,5.68714 -0.55313,12.95355 -7.28968,12.95355 -1.25225,0 -4.29453,-1.20187 -9.08226,-3.58799 l -7.19927,-3.58796 -7.37197,3.59617 c -8.07507,3.93914 -10.21699,4.34922 -13.52752,2.59 z"
|
||||
id="path948" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
10
.idea/jarRepositories.xml
generated
@@ -31,15 +31,5 @@
|
||||
<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>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://maven.pkg.github.com/nv95/kotatsu-parsers" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/ktlint.xml
generated
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KtlintProjectConfiguration">
|
||||
<enableKtlint>false</enableKtlint>
|
||||
<androidMode>true</androidMode>
|
||||
<treatAsErrors>false</treatAsErrors>
|
||||
<disabledRules>
|
||||
<list>
|
||||
<option value="no-empty-first-line-in-method-block" />
|
||||
</list>
|
||||
</disabledRules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?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>
|
||||
6
.idea/render.experimental.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RenderSettings">
|
||||
<option name="quality" value="0.25" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?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>
|
||||
9
.idea/vcs.xml
generated
@@ -1,14 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitSharedSettings">
|
||||
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
||||
<list>
|
||||
<option value="master" />
|
||||
<option value="devel" />
|
||||
<option value="legacy" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
|
||||
15
.travis.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools-29.0.6
|
||||
- build-tools-29.0.3
|
||||
- android-29
|
||||
licenses:
|
||||
- android-sdk-preview-license-.+
|
||||
- android-sdk-license-.+
|
||||
- google-gdk-license-.+
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
||||
3
.weblate
@@ -1,3 +0,0 @@
|
||||
[weblate]
|
||||
url = https://hosted.weblate.org/api/
|
||||
translation = kotatsu/strings
|
||||
@@ -1,11 +0,0 @@
|
||||
## Kotatsu contribution guidelines
|
||||
|
||||
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/KotatsuApp/Kotatsu/issues), please, assign this issue to you and/or comment about it.
|
||||
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
|
||||
- Translations have to be managed using the [Weblate](https://hosted.weblate.org/engage/kotatsu/) platform.
|
||||
- In case you want to add a new manga source, refer to the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers).
|
||||
|
||||
Refactoring or some dev-faces improvements are also might be accepted, however please stick to the following principles:
|
||||
- Performance matters. In the case of choosing between source code beauty and performance, performance should be a priority.
|
||||
- Please, do not modify readme and other information files (except for typos).
|
||||
- Avoid adding new dependencies unless required. APK size is important.
|
||||
61
README.md
@@ -1,57 +1,40 @@
|
||||
# Kotatsu
|
||||
# Kotatsu
|
||||
|
||||
Kotatsu is a free and open source manga reader for Android.
|
||||
|
||||
   [](https://hosted.weblate.org/engage/kotatsu/) [](https://t.me/kotatsuapp) [](https://discord.gg/NNJ5RgVBC5)
|
||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||
|
||||
### Download
|
||||
|
||||
- **Recommended:** Download and install APK from **[GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. Application has a built-in self-updating feature.
|
||||
- Get it on **[F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu)**. The F-Droid build may be a bit outdated and some fixes might be missing.
|
||||
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
|
||||
|
||||
### Main Features
|
||||
|
||||
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
|
||||
* Search manga by name and genres
|
||||
* Reading history and bookmarks
|
||||
* Favourites organized by user-defined categories
|
||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
||||
* Tablet-optimized Material You UI
|
||||
* 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
|
||||
* Standard and Webtoon-optimized reader
|
||||
* Notifications about new chapters with updates feed
|
||||
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
|
||||
* Password/fingerprint protect access to the app
|
||||
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
|
||||
* Notifications about new chapters
|
||||
|
||||
### Screenshots
|
||||
|
||||
|  |  |  |
|
||||
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
|  |  |
|
||||
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
|
||||
### Localization
|
||||
|
||||
[<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
|
||||
|
||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
||||
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
||||
|
||||
### Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||
|  |  |  |
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
|
||||
### License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
Kotatsu is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
|
||||
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
|
||||
install instructions.
|
||||
### Disclaimer
|
||||
|
||||
### DMCA disclaimer
|
||||
|
||||
The developers of this application does not have any affiliation with the content available in the app.
|
||||
It is collecting from the sources freely available through any web browser.
|
||||
The developers of this application does not have any affiliation with the content providers available.
|
||||
201
app/build.gradle
@@ -1,160 +1,101 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'kotlin-parcelize'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
}
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
buildToolsVersion = '34.0.0'
|
||||
namespace = 'org.koitharu.kotatsu'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 584
|
||||
versionName = '6.1.6'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
minSdkVersion 16
|
||||
maxSdkVersion 20
|
||||
targetSdkVersion 29
|
||||
versionCode gitCommits
|
||||
versionName '0.3'
|
||||
|
||||
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
kapt {
|
||||
arguments {
|
||||
arg('room.schemaLocation', "$projectDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
archivesBaseName = "kotatsu_${gitCommits}"
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = '.debug'
|
||||
}
|
||||
release {
|
||||
multiDexEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
main.java.srcDirs += 'src/main/kotlin/'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
freeCompilerArgs += [
|
||||
'-opt-in=kotlin.ExperimentalStdlibApi',
|
||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
||||
]
|
||||
}
|
||||
lint {
|
||||
abortOnError true
|
||||
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
abortOnError false
|
||||
}
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
unitTests.returnDefaultValues false
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
||||
}
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
afterEvaluate {
|
||||
compileDebugKotlin {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
||||
}
|
||||
}
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:400a90464e') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'androidx.core:core-ktx:1.3.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.4'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
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.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
|
||||
implementation 'androidx.room:room-runtime:2.2.5'
|
||||
implementation 'androidx.room:room-ktx:2.2.5'
|
||||
kapt 'androidx.room:room-compiler:2.2.5'
|
||||
|
||||
// TODO https://issuetracker.google.com/issues/254846063
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
//noinspection GradleDependency
|
||||
implementation('com.google.guava:guava:32.0.1-android') {
|
||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
}
|
||||
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 'androidx.room:room-runtime:2.5.2'
|
||||
implementation 'androidx.room:room-ktx:2.5.2'
|
||||
ksp 'androidx.room:room-compiler:2.5.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||
implementation 'com.squareup.okio:okio:2.5.0'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||
implementation 'com.squareup.okio:okio:3.6.0'
|
||||
implementation 'org.koin:koin-android:2.1.5'
|
||||
implementation 'io.coil-kt:coil:0.9.5'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
implementation 'com.tomclaw.cache:cache:1.0'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
|
||||
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
|
||||
|
||||
implementation 'com.google.dagger:hilt-android:2.48.1'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.48.1'
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:169806d928'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
|
||||
implementation 'ch.acra:acra-http:5.11.2'
|
||||
implementation 'ch.acra:acra-dialog:5.11.2'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20230618'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48.1'
|
||||
}
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.json:json:20190722'
|
||||
}
|
||||
14
app/proguard-rules.pro
vendored
@@ -1,4 +1,3 @@
|
||||
-optimizationpasses 8
|
||||
-dontobfuscate
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void checkExpressionValueIsNotNull(...);
|
||||
@@ -6,15 +5,8 @@
|
||||
public static void checkReturnedValueIsNotNull(...);
|
||||
public static void checkFieldIsNotNull(...);
|
||||
public static void checkParameterIsNotNull(...);
|
||||
public static void checkNotNullParameter(...);
|
||||
}
|
||||
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
|
||||
public <init>(...);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Read later",
|
||||
"sortKey": 1,
|
||||
"order": "NEWEST",
|
||||
"createdAt": 1335906000000,
|
||||
"isTrackingEnabled": true,
|
||||
"isVisibleInLibrary": true
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||
"chapters": [
|
||||
{
|
||||
"id": 1552943969433540704,
|
||||
"name": "1 - 1",
|
||||
"number": 1,
|
||||
"url": "/stranstviia_emanon/vol1/1",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433540705,
|
||||
"name": "1 - 2",
|
||||
"number": 2,
|
||||
"url": "/stranstviia_emanon/vol1/2",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433540706,
|
||||
"name": "1 - 3",
|
||||
"number": 3,
|
||||
"url": "/stranstviia_emanon/vol1/3",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433540707,
|
||||
"name": "1 - 4",
|
||||
"number": 4,
|
||||
"url": "/stranstviia_emanon/vol1/4",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433540708,
|
||||
"name": "1 - 5",
|
||||
"number": 5,
|
||||
"url": "/stranstviia_emanon/vol1/5",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541665,
|
||||
"name": "2 - 1",
|
||||
"number": 6,
|
||||
"url": "/stranstviia_emanon/vol2/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1415570400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541666,
|
||||
"name": "2 - 2",
|
||||
"number": 7,
|
||||
"url": "/stranstviia_emanon/vol2/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1419976800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541667,
|
||||
"name": "2 - 3",
|
||||
"number": 8,
|
||||
"url": "/stranstviia_emanon/vol2/3",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1427922000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541668,
|
||||
"name": "2 - 4",
|
||||
"number": 9,
|
||||
"url": "/stranstviia_emanon/vol2/4",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1436907600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541669,
|
||||
"name": "2 - 5",
|
||||
"number": 10,
|
||||
"url": "/stranstviia_emanon/vol2/5",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1446674400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433541670,
|
||||
"name": "2 - 6",
|
||||
"number": 11,
|
||||
"url": "/stranstviia_emanon/vol2/6",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1451512800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433542626,
|
||||
"name": "3 - 1",
|
||||
"number": 12,
|
||||
"url": "/stranstviia_emanon/vol3/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433542627,
|
||||
"name": "3 - 2",
|
||||
"number": 13,
|
||||
"url": "/stranstviia_emanon/vol3/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 1552943969433542628,
|
||||
"name": "3 - 3",
|
||||
"number": 14,
|
||||
"url": "/stranstviia_emanon/vol3/3",
|
||||
"scanlator": "",
|
||||
"uploadDate": 1465851600000,
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||
"chapters": [],
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||
"chapters": [
|
||||
{
|
||||
"id": 3552943969433540704,
|
||||
"name": "1 - 1",
|
||||
"number": 1,
|
||||
"url": "/stranstviia_emanon/vol1/1",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540705,
|
||||
"name": "1 - 2",
|
||||
"number": 2,
|
||||
"url": "/stranstviia_emanon/vol1/2",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540706,
|
||||
"name": "1 - 3",
|
||||
"number": 3,
|
||||
"url": "/stranstviia_emanon/vol1/3",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540707,
|
||||
"name": "1 - 4",
|
||||
"number": 4,
|
||||
"url": "/stranstviia_emanon/vol1/4",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540708,
|
||||
"name": "1 - 5",
|
||||
"number": 5,
|
||||
"url": "/stranstviia_emanon/vol1/5",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541665,
|
||||
"name": "2 - 1",
|
||||
"number": 6,
|
||||
"url": "/stranstviia_emanon/vol2/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1415570400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541666,
|
||||
"name": "2 - 2",
|
||||
"number": 7,
|
||||
"url": "/stranstviia_emanon/vol2/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1419976800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541667,
|
||||
"name": "2 - 3",
|
||||
"number": 8,
|
||||
"url": "/stranstviia_emanon/vol2/3",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1427922000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541668,
|
||||
"name": "2 - 4",
|
||||
"number": 9,
|
||||
"url": "/stranstviia_emanon/vol2/4",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1436907600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541669,
|
||||
"name": "2 - 5",
|
||||
"number": 10,
|
||||
"url": "/stranstviia_emanon/vol2/5",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1446674400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541670,
|
||||
"name": "2 - 6",
|
||||
"number": 11,
|
||||
"url": "/stranstviia_emanon/vol2/6",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1451512800000,
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||
"chapters": [
|
||||
{
|
||||
"id": 3552943969433540704,
|
||||
"name": "1 - 1",
|
||||
"number": 1,
|
||||
"url": "/stranstviia_emanon/vol1/1",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540705,
|
||||
"name": "1 - 2",
|
||||
"number": 2,
|
||||
"url": "/stranstviia_emanon/vol1/2",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540706,
|
||||
"name": "1 - 3",
|
||||
"number": 3,
|
||||
"url": "/stranstviia_emanon/vol1/3",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540707,
|
||||
"name": "1 - 4",
|
||||
"number": 4,
|
||||
"url": "/stranstviia_emanon/vol1/4",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540708,
|
||||
"name": "1 - 5",
|
||||
"number": 5,
|
||||
"url": "/stranstviia_emanon/vol1/5",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541665,
|
||||
"name": "2 - 1",
|
||||
"number": 6,
|
||||
"url": "/stranstviia_emanon/vol2/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1415570400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541666,
|
||||
"name": "2 - 2",
|
||||
"number": 7,
|
||||
"url": "/stranstviia_emanon/vol2/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1419976800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541667,
|
||||
"name": "2 - 3",
|
||||
"number": 8,
|
||||
"url": "/stranstviia_emanon/vol2/3",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1427922000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541668,
|
||||
"name": "2 - 4",
|
||||
"number": 9,
|
||||
"url": "/stranstviia_emanon/vol2/4",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1436907600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541669,
|
||||
"name": "2 - 5",
|
||||
"number": 10,
|
||||
"url": "/stranstviia_emanon/vol2/5",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1446674400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541670,
|
||||
"name": "2 - 6",
|
||||
"number": 11,
|
||||
"url": "/stranstviia_emanon/vol2/6",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1451512800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542626,
|
||||
"name": "3 - 1",
|
||||
"number": 12,
|
||||
"url": "/stranstviia_emanon/vol3/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542627,
|
||||
"name": "3 - 2",
|
||||
"number": 13,
|
||||
"url": "/stranstviia_emanon/vol3/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542628,
|
||||
"name": "3 - 3",
|
||||
"number": 14,
|
||||
"url": "/stranstviia_emanon/vol3/3",
|
||||
"scanlator": "",
|
||||
"uploadDate": 1465851600000,
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": null,
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
{
|
||||
"id": -2096681732556647985,
|
||||
"title": "Странствия Эманон",
|
||||
"url": "/stranstviia_emanon",
|
||||
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||
"rating": 0.9400894,
|
||||
"isNsfw": true,
|
||||
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||
"tags": [
|
||||
{
|
||||
"title": "Сверхъестественное",
|
||||
"key": "supernatural",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Сэйнэн",
|
||||
"key": "seinen",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Повседневность",
|
||||
"key": "slice_of_life",
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"title": "Приключения",
|
||||
"key": "adventure",
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"state": "FINISHED",
|
||||
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||
"chapters": [
|
||||
{
|
||||
"id": 3552943969433540704,
|
||||
"name": "1 - 1",
|
||||
"number": 1,
|
||||
"url": "/stranstviia_emanon/vol1/1",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540705,
|
||||
"name": "1 - 2",
|
||||
"number": 2,
|
||||
"url": "/stranstviia_emanon/vol1/2",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540706,
|
||||
"name": "1 - 3",
|
||||
"number": 3,
|
||||
"url": "/stranstviia_emanon/vol1/3",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540707,
|
||||
"name": "1 - 4",
|
||||
"number": 4,
|
||||
"url": "/stranstviia_emanon/vol1/4",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433540708,
|
||||
"name": "1 - 5",
|
||||
"number": 5,
|
||||
"url": "/stranstviia_emanon/vol1/5",
|
||||
"scanlator": "Sad-Robot",
|
||||
"uploadDate": 1342731600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541666,
|
||||
"name": "2 - 2",
|
||||
"number": 7,
|
||||
"url": "/stranstviia_emanon/vol2/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1419976800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541667,
|
||||
"name": "2 - 3",
|
||||
"number": 8,
|
||||
"url": "/stranstviia_emanon/vol2/3",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1427922000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541668,
|
||||
"name": "2 - 4",
|
||||
"number": 9,
|
||||
"url": "/stranstviia_emanon/vol2/4",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1436907600000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541669,
|
||||
"name": "2 - 5",
|
||||
"number": 10,
|
||||
"url": "/stranstviia_emanon/vol2/5",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1446674400000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433541670,
|
||||
"name": "2 - 6",
|
||||
"number": 11,
|
||||
"url": "/stranstviia_emanon/vol2/6",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1451512800000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542626,
|
||||
"name": "3 - 1",
|
||||
"number": 12,
|
||||
"url": "/stranstviia_emanon/vol3/1",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542627,
|
||||
"name": "3 - 2",
|
||||
"number": 13,
|
||||
"url": "/stranstviia_emanon/vol3/2",
|
||||
"scanlator": "Sup!",
|
||||
"uploadDate": 1461618000000,
|
||||
"source": "READMANGA_RU"
|
||||
},
|
||||
{
|
||||
"id": 3552943969433542628,
|
||||
"name": "3 - 3",
|
||||
"number": 14,
|
||||
"url": "/stranstviia_emanon/vol3/3",
|
||||
"scanlator": "",
|
||||
"uploadDate": 1465851600000,
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
],
|
||||
"source": "READMANGA_RU"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
class HiltTestRunner : AndroidJUnitRunner() {
|
||||
|
||||
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Instrumentation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
|
||||
waitForIdle { cont.resume(Unit) }
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.squareup.moshi.*
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object SampleData {
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(DateAdapter())
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
val manga: Manga = loadAsset("manga/header.json", Manga::class)
|
||||
|
||||
val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class)
|
||||
|
||||
val tag = mangaDetails.tags.elementAt(2)
|
||||
|
||||
val chapter = checkNotNull(mangaDetails.chapters)[2]
|
||||
|
||||
val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class)
|
||||
|
||||
fun <T : Any> loadAsset(name: String, cls: KClass<T>): T {
|
||||
val assets = InstrumentationRegistry.getInstrumentation().context.assets
|
||||
return assets.open(name).use {
|
||||
moshi.adapter(cls.java).fromJson(it.source().buffer())
|
||||
} ?: throw RuntimeException("Cannot read asset from json \"$name\"")
|
||||
}
|
||||
|
||||
private class DateAdapter : JsonAdapter<Date>() {
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): Date? {
|
||||
val ms = reader.nextLong()
|
||||
return if (ms == 0L) {
|
||||
null
|
||||
} else {
|
||||
Date(ms)
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
override fun toJson(writer: JsonWriter, value: Date?) {
|
||||
writer.value(value?.time ?: 0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MangaDatabaseTest {
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
MangaDatabase::class.java,
|
||||
)
|
||||
|
||||
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
|
||||
@Test
|
||||
fun versions() {
|
||||
assertEquals(1, migrations.first().startVersion)
|
||||
repeat(migrations.size) { i ->
|
||||
assertEquals(i + 1, migrations[i].startVersion)
|
||||
assertEquals(i + 2, migrations[i].endVersion)
|
||||
}
|
||||
assertEquals(DATABASE_VERSION, migrations.last().endVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateAll() {
|
||||
helper.createDatabase(TEST_DB, 1).close()
|
||||
for (migration in migrations) {
|
||||
helper.runMigrationsAndValidate(
|
||||
TEST_DB,
|
||||
migration.endVersion,
|
||||
true,
|
||||
migration,
|
||||
).close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prePopulate() {
|
||||
val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources
|
||||
helper.createDatabase(TEST_DB, DATABASE_VERSION).use {
|
||||
DatabasePrePopulateCallback(resources).onCreate(it)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TEST_DB = "test-db"
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koitharu.kotatsu.SampleData
|
||||
import org.koitharu.kotatsu.awaitForIdle
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppShortcutManagerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
@Inject
|
||||
lateinit var database: MangaDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
database.clearAllTables()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateShortcuts() = runTest {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
||||
return@runTest
|
||||
}
|
||||
database.invalidationTracker.addObserver(appShortcutManager)
|
||||
awaitUpdate()
|
||||
assertTrue(getShortcuts().isEmpty())
|
||||
historyRepository.addOrUpdate(
|
||||
manga = SampleData.manga,
|
||||
chapterId = SampleData.chapter.id,
|
||||
page = 4,
|
||||
scroll = 2,
|
||||
percent = 0.3f,
|
||||
)
|
||||
awaitUpdate()
|
||||
|
||||
val shortcuts = getShortcuts()
|
||||
assertEquals(1, shortcuts.size)
|
||||
}
|
||||
|
||||
private fun getShortcuts(): List<ShortcutInfo> {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val manager = checkNotNull(context.getSystemService<ShortcutManager>())
|
||||
return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" }
|
||||
}
|
||||
|
||||
private suspend fun awaitUpdate() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
instrumentation.awaitForIdle()
|
||||
appShortcutManager.await()
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koitharu.kotatsu.SampleData
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppBackupAgentTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var favouritesRepository: FavouritesRepository
|
||||
|
||||
@Inject
|
||||
lateinit var backupRepository: BackupRepository
|
||||
|
||||
@Inject
|
||||
lateinit var database: MangaDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
database.clearAllTables()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backupAndRestore() = runTest {
|
||||
val category = favouritesRepository.createCategory(
|
||||
title = SampleData.favouriteCategory.title,
|
||||
sortOrder = SampleData.favouriteCategory.order,
|
||||
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
|
||||
isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary,
|
||||
)
|
||||
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
|
||||
historyRepository.addOrUpdate(
|
||||
manga = SampleData.mangaDetails,
|
||||
chapterId = SampleData.mangaDetails.chapters!![2].id,
|
||||
page = 3,
|
||||
scroll = 40,
|
||||
percent = 0.2f,
|
||||
)
|
||||
val history = checkNotNull(historyRepository.getOne(SampleData.manga))
|
||||
|
||||
val agent = AppBackupAgent()
|
||||
val backup = agent.createBackupFile(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
repository = backupRepository,
|
||||
)
|
||||
|
||||
database.clearAllTables()
|
||||
assertTrue(favouritesRepository.getAllManga().isEmpty())
|
||||
assertNull(historyRepository.getLastOrNull())
|
||||
|
||||
backup.inputStream().use {
|
||||
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||
}
|
||||
|
||||
assertEquals(category, favouritesRepository.getCategory(category.id))
|
||||
assertEquals(history, historyRepository.getOne(SampleData.manga))
|
||||
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
||||
|
||||
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
|
||||
assertTrue(SampleData.tag in allTags)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restoreOldBackup() {
|
||||
val agent = AppBackupAgent()
|
||||
val backup = File.createTempFile("backup_", ".tmp")
|
||||
InstrumentationRegistry.getInstrumentation().context.assets
|
||||
.open("kotatsu_test.bak", AssetManager.ACCESS_STREAMING)
|
||||
.use { input ->
|
||||
backup.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
backup.inputStream().use {
|
||||
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||
}
|
||||
runTest {
|
||||
assertEquals(6, historyRepository.observeAll().first().size)
|
||||
assertEquals(2, favouritesRepository.observeCategories().first().size)
|
||||
assertEquals(15, favouritesRepository.getAllManga().size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package org.koitharu.kotatsu.tracker.domain
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import junit.framework.TestCase.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koitharu.kotatsu.SampleData
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TrackerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var repository: TrackingRepository
|
||||
|
||||
@Inject
|
||||
lateinit var dataRepository: MangaDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var tracker: Tracker
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noUpdates() = runTest {
|
||||
val manga = loadManga("full.json")
|
||||
tracker.deleteTrack(manga.id)
|
||||
|
||||
tracker.checkUpdates(manga, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||
tracker.checkUpdates(manga, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasUpdates() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaFull = loadManga("full.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badIds() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaBad = loadManga("bad_ids.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badIds2() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaBad = loadManga("bad_ids.json")
|
||||
val mangaFull = loadManga("full.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullReset() = runTest {
|
||||
val mangaFull = loadManga("full.json")
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaEmpty = loadManga("empty.json")
|
||||
tracker.deleteTrack(mangaFull.id)
|
||||
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncWithHistory() = runTest {
|
||||
val mangaFull = loadManga("full.json")
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
tracker.deleteTrack(mangaFull.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
||||
repository.syncWithHistory(mangaFull, chapter.id)
|
||||
|
||||
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
|
||||
repository.syncWithHistory(mangaFull, chapter.id)
|
||||
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
private suspend fun loadManga(name: String): Manga {
|
||||
val manga = SampleData.loadAsset("manga/$name", Manga::class)
|
||||
dataRepository.storeManga(manga)
|
||||
return manga
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.content.Context
|
||||
import android.os.StrictMode
|
||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||
import org.koitharu.kotatsu.core.BaseApp
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
|
||||
class KotatsuApp : BaseApp() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
enableStrictMode()
|
||||
}
|
||||
|
||||
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)
|
||||
.setClassInstanceLimit(PageLoader::class.java, 1)
|
||||
.penaltyLog()
|
||||
.build(),
|
||||
)
|
||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
||||
.penaltyDeath()
|
||||
.detectFragmentReuse()
|
||||
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
|
||||
.detectRetainInstanceUsage()
|
||||
.detectSetUserVisibleHint()
|
||||
.detectFragmentTagUsage()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders.ACCEPT_ENCODING
|
||||
|
||||
class CurlLoggingInterceptor(
|
||||
private val curlOptions: String? = null
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
var isCompressed = false
|
||||
|
||||
val curlCmd = StringBuilder()
|
||||
curlCmd.append("curl")
|
||||
if (curlOptions != null) {
|
||||
curlCmd.append(' ').append(curlOptions)
|
||||
}
|
||||
curlCmd.append(" -X ").append(request.method)
|
||||
|
||||
for ((name, value) in request.headers) {
|
||||
if (name.equals(ACCEPT_ENCODING, ignoreCase = true) && value.equals("gzip", ignoreCase = true)) {
|
||||
isCompressed = true
|
||||
}
|
||||
curlCmd.append(" -H \"").append(name).append(": ").append(value.escape()).append('\"')
|
||||
}
|
||||
|
||||
val body = request.body
|
||||
if (body != null) {
|
||||
val buffer = Buffer()
|
||||
body.writeTo(buffer)
|
||||
val charset = body.contentType()?.charset() ?: Charsets.UTF_8
|
||||
curlCmd.append(" --data-raw '")
|
||||
.append(buffer.readString(charset).replace("\n", "\\n"))
|
||||
.append("'")
|
||||
}
|
||||
if (isCompressed) {
|
||||
curlCmd.append(" --compressed")
|
||||
}
|
||||
curlCmd.append(" \"").append(request.url).append('"')
|
||||
|
||||
log("---cURL (" + request.url + ")")
|
||||
log(curlCmd.toString())
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun String.escape() = replace("\"", "\\\"")
|
||||
|
||||
private fun log(msg: String) {
|
||||
Log.d("CURL", msg)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* This parser is just for parser development, it should not be used in releases
|
||||
*/
|
||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("")
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||
17
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@id/action_leaks"
|
||||
android:title="@string/leak_canary_display_activity_label"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
||||
</resources>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="account_type_sync" translatable="false">org.kotatsu.debug.sync</string>
|
||||
<string name="sync_authority_history" translatable="false">org.koitharu.kotatsu.debug.history</string>
|
||||
<string name="sync_authority_favourites" translatable="false">org.koitharu.kotatsu.debug.favourites</string>
|
||||
</resources>
|
||||
4
app/src/debug/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Kotatsu Dev</string>
|
||||
</resources>
|
||||
125
app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
@@ -0,0 +1,125 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.room.Room
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.util.CoilUtils
|
||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
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.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
|
||||
|
||||
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()
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
initKoin()
|
||||
initCoil()
|
||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||
if (BuildConfig.DEBUG) {
|
||||
initErrorHandler()
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
|
||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||
FavouritesRepository.subscribe(widgetUpdater)
|
||||
HistoryRepository.subscribe(widgetUpdater)
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(applicationContext)
|
||||
modules(
|
||||
module {
|
||||
factory {
|
||||
okHttp()
|
||||
.cache(CacheUtils.createHttpCache(applicationContext))
|
||||
.build()
|
||||
}
|
||||
single {
|
||||
mangaDb().build()
|
||||
}
|
||||
single {
|
||||
MangaLoaderContext()
|
||||
}
|
||||
factory {
|
||||
AppSettings(applicationContext)
|
||||
}
|
||||
single {
|
||||
PagesCache(applicationContext)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCoil() {
|
||||
Coil.setDefaultImageLoader(ImageLoader(applicationContext) {
|
||||
okHttpClient {
|
||||
okHttp()
|
||||
.cache(CoilUtils.createDefaultCache(applicationContext))
|
||||
.build()
|
||||
}
|
||||
componentRegistry {
|
||||
add(CbzFetcher())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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 :orderBy LIMIT :limit OFFSET :offset")
|
||||
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): 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)
|
||||
}
|
||||
47
app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
42
app/src/main/java/org/koitharu/kotatsu/core/db/MangaDao.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Dao
|
||||
abstract class MangaDao {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): MangaWithTags?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(manga: MangaEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun update(manga: MangaEntity): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insertTagRelation(tag: MangaTagsEntity): Long
|
||||
|
||||
@Query("DELETE FROM manga_tags WHERE manga_id = :mangaId")
|
||||
abstract suspend fun clearTagRelation(mangaId: Long)
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
|
||||
if (update(manga) <= 0) {
|
||||
insert(manga)
|
||||
if (tags != null) {
|
||||
clearTagRelation(manga.id)
|
||||
tags.map {
|
||||
MangaTagsEntity(manga.id, it.id)
|
||||
}.forEach {
|
||||
insertTagRelation(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
|
||||
], version = 4
|
||||
)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
abstract val historyDao: HistoryDao
|
||||
|
||||
abstract val tagsDao: TagsDao
|
||||
|
||||
abstract val mangaDao: MangaDao
|
||||
|
||||
abstract val favouritesDao: FavouritesDao
|
||||
|
||||
abstract val preferencesDao: PreferencesDao
|
||||
|
||||
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
||||
|
||||
abstract val tracksDao: TracksDao
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
|
||||
@Dao
|
||||
abstract class PreferencesDao {
|
||||
|
||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
||||
abstract suspend fun find(mangaId: Long): MangaPrefsEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(pref: MangaPrefsEntity): Long
|
||||
|
||||
@Update
|
||||
abstract suspend fun update(pref: MangaPrefsEntity): Int
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(pref: MangaPrefsEntity) {
|
||||
if (update(pref) == 0) {
|
||||
insert(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
app/src/main/java/org/koitharu/kotatsu/core/db/TagsDao.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Dao
|
||||
interface TagsDao {
|
||||
|
||||
@Query("SELECT * FROM tags")
|
||||
suspend fun getAllTags(): List<TagEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insert(tag: TagEntity): Long
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun update(tag: TagEntity): Int
|
||||
|
||||
@Transaction
|
||||
suspend fun upsert(tags: Iterable<TagEntity>) {
|
||||
tags.forEach { tag ->
|
||||
if (update(tag) <= 0) {
|
||||
insert(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/org/koitharu/kotatsu/core/db/TracksDao.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
||||
|
||||
|
||||
@Dao
|
||||
abstract class TracksDao {
|
||||
|
||||
@Query("SELECT * FROM tracks")
|
||||
abstract suspend fun findAll(): List<TrackEntity>
|
||||
|
||||
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract suspend fun find(mangaId: Long): TrackEntity?
|
||||
|
||||
@Query("DELETE FROM tracks")
|
||||
abstract suspend fun clear()
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: TrackEntity): Long
|
||||
|
||||
@Update
|
||||
abstract suspend fun update(entity: TrackEntity): Int
|
||||
|
||||
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract suspend fun delete(mangaId: Long)
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(entity: TrackEntity) {
|
||||
if (update(entity) == 0) {
|
||||
insert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "favourite_categories")
|
||||
data class FavouriteCategoryEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "category_id") val categoryId: Int,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
@ColumnInfo(name = "title") val title: String
|
||||
) {
|
||||
|
||||
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
|
||||
id = id ?: categoryId.toLong(),
|
||||
title = title,
|
||||
createdAt = Date(createdAt)
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_FAVOURITES,
|
||||
primaryKeys = ["manga_id", "category_id"],
|
||||
foreignKeys = [
|
||||
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
@@ -27,7 +23,5 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
data class FavouriteEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
||||
@ColumnInfo(name = "sort_key") val sortKey: Int,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
|
||||
)
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long
|
||||
)
|
||||
@@ -1,13 +1,10 @@
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
class FavouriteManga(
|
||||
data class FavouriteManga(
|
||||
@Embedded val favourite: FavouriteEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
@@ -1,31 +1,37 @@
|
||||
package org.koitharu.kotatsu.history.data
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import java.util.*
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_HISTORY,
|
||||
foreignKeys = [
|
||||
tableName = "history", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class HistoryEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "page") val page: Int,
|
||||
@ColumnInfo(name = "scroll") val scroll: Float,
|
||||
@ColumnInfo(name = "percent") val percent: Float,
|
||||
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
|
||||
)
|
||||
@ColumnInfo(name = "scroll") val scroll: Float
|
||||
) {
|
||||
|
||||
fun toMangaHistory() = MangaHistory(
|
||||
createdAt = Date(createdAt),
|
||||
updatedAt = Date(updatedAt),
|
||||
chapterId = chapterId,
|
||||
page = page,
|
||||
scroll = scroll
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
package org.koitharu.kotatsu.history.data
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
class HistoryWithManga(
|
||||
data class HistoryWithManga(
|
||||
@Embedded val history: HistoryEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
@@ -19,5 +16,5 @@ class HistoryWithManga(
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>,
|
||||
val tags: List<TagEntity>
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaState
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
|
||||
@Entity(tableName = "manga")
|
||||
data 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 = "url") val url: String,
|
||||
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
|
||||
@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 = "source") val source: String
|
||||
) {
|
||||
|
||||
fun toManga(tags: Set<MangaTag> = emptySet()) = Manga(
|
||||
id = this.id,
|
||||
title = this.title,
|
||||
altTitle = this.altTitle,
|
||||
state = this.state?.let { MangaState.valueOf(it) },
|
||||
rating = this.rating,
|
||||
url = this.url,
|
||||
coverUrl = this.coverUrl,
|
||||
largeCoverUrl = this.largeCoverUrl,
|
||||
author = this.author,
|
||||
source = MangaSource.valueOf(this.source),
|
||||
tags = tags
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(manga: Manga) = MangaEntity(
|
||||
id = manga.id,
|
||||
url = manga.url,
|
||||
source = manga.source.name,
|
||||
largeCoverUrl = manga.largeCoverUrl,
|
||||
coverUrl = manga.coverUrl,
|
||||
altTitle = manga.altTitle,
|
||||
rating = manga.rating,
|
||||
state = manga.state?.name,
|
||||
title = manga.title,
|
||||
author = manga.author
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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 = "preferences", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class MangaPrefsEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "mode") val mode: Int
|
||||
)
|
||||
@@ -3,27 +3,24 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_MANGA_TAGS,
|
||||
primaryKeys = ["manga_id", "tag_id"],
|
||||
foreignKeys = [
|
||||
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = TagEntity::class,
|
||||
parentColumns = ["tag_id"],
|
||||
childColumns = ["tag_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
class MangaTagsEntity(
|
||||
data class MangaTagsEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long,
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
|
||||
)
|
||||
@@ -11,5 +11,10 @@ data class MangaWithTags(
|
||||
entityColumn = "tag_id",
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>,
|
||||
)
|
||||
val tags: List<TagEntity>
|
||||
) {
|
||||
|
||||
fun toManga() = manga.toManga(tags.map {
|
||||
it.toMangaTag()
|
||||
}.toSet())
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
|
||||
@Entity(tableName = "tags")
|
||||
data class TagEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "tag_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
fun toMangaTag() = MangaTag(
|
||||
key = this.key,
|
||||
title = this.title,
|
||||
source = MangaSource.valueOf(this.source)
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromMangaTag(tag: MangaTag) = TagEntity(
|
||||
title = tag.title,
|
||||
key = tag.key,
|
||||
source = tag.source.name,
|
||||
id = "${tag.key}_${tag.source.name}".longHashCode()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,26 @@
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "tracks",
|
||||
foreignKeys = [
|
||||
tableName = "tracks", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
class TrackEntity(
|
||||
data class TrackEntity (
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
|
||||
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
||||
@ColumnInfo(name = "last_chapter_id") val lastChapterId: Long,
|
||||
@ColumnInfo(name = "chapters_new") val newChapters: Int,
|
||||
@ColumnInfo(name = "last_check") val lastCheck: Long,
|
||||
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
|
||||
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
|
||||
)
|
||||
)
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration1To2 : Migration(1, 2) {
|
||||
object 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
|
||||
|
||||
class Migration2To3 : Migration(2, 3) {
|
||||
object 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
|
||||
|
||||
class Migration3To4 : Migration(3, 4) {
|
||||
object 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,5 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
import java.lang.NullPointerException
|
||||
|
||||
class MangaNotFoundException(s: String? = null) : RuntimeException(s)
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AppVersion(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val apkSize: Long,
|
||||
val apkUrl: String
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
import org.koitharu.kotatsu.utils.ext.parseJson
|
||||
|
||||
class GithubRepository : KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
|
||||
suspend fun getLatestVersion(): AppVersion {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/nv95/Kotatsu/releases/latest")
|
||||
val json = okHttp.newCall(request.build()).await().parseJson()
|
||||
val asset = json.getJSONArray("assets").getJSONObject(0)
|
||||
return AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class VersionId(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val build: Int,
|
||||
val variantType: String,
|
||||
val variantNumber: Int
|
||||
) : Comparable<VersionId> {
|
||||
|
||||
override fun compareTo(other: VersionId): Int {
|
||||
var diff = major.compareTo(other.major)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = minor.compareTo(other.minor)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = build.compareTo(other.build)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = variantWeight(variantType).compareTo(variantWeight(other.variantType))
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
return variantNumber.compareTo(other.variantNumber)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
private fun variantWeight(variantType: String) =
|
||||
when (variantType.toLowerCase(Locale.ROOT)) {
|
||||
"a", "alpha" -> 1
|
||||
"b", "beta" -> 2
|
||||
"rc" -> 4
|
||||
"" -> 8
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(versionName: String): VersionId {
|
||||
val parts = versionName.substringBeforeLast('-').split('.')
|
||||
val variant = versionName.substringAfterLast('-', "")
|
||||
return VersionId(
|
||||
major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
|
||||
minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
|
||||
variantType = variant.filter(Char::isLetter),
|
||||
variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.local
|
||||
|
||||
enum class Cache(val dir: String) {
|
||||
|
||||
THUMBS("image_cache"),
|
||||
PAGES("pages");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.koitharu.kotatsu.core.local
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil.bitmappool.BitmapPool
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.Options
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.size.Size
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class CbzFetcher : Fetcher<Uri> {
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: Uri,
|
||||
size: Size,
|
||||
options: Options
|
||||
): FetchResult {
|
||||
val zip = ZipFile(data.schemeSpecificPart)
|
||||
val entry = zip.getEntry(data.fragment)
|
||||
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
|
||||
return SourceResult(
|
||||
source = zip.getInputStream(entry).source().buffer(),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext),
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
override fun key(data: Uri): String? = data.toString()
|
||||
|
||||
override fun handles(data: Uri) = data.scheme == "cbz"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.local
|
||||
|
||||
import java.io.File
|
||||
import java.io.FilenameFilter
|
||||
|
||||
class CbzFilter : FilenameFilter {
|
||||
|
||||
override fun accept(dir: File, name: String) =
|
||||
name.endsWith(".cbz", ignoreCase = true) || name.endsWith(".zip", ignoreCase = true)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.local
|
||||
|
||||
import android.content.Context
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.sub
|
||||
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
|
||||
class PagesCache(context: Context) {
|
||||
|
||||
private val cacheDir = context.externalCacheDir ?: context.cacheDir
|
||||
private val lruCache = DiskLruCache.create(cacheDir.sub(Cache.PAGES.dir), FileSizeUtils.mbToBytes(200))
|
||||
|
||||
operator fun get(url: String): File? {
|
||||
return lruCache.get(url)?.takeIfReadable()
|
||||
}
|
||||
|
||||
fun put(url: String, writer: (OutputStream) -> Unit): File {
|
||||
val file = cacheDir.sub(url.longHashCode().toString())
|
||||
file.outputStream().use(writer)
|
||||
val res = lruCache.put(url, file)
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.koitharu.kotatsu.core.local.cookies
|
||||
|
||||
import okhttp3.CookieJar
|
||||
|
||||
/**
|
||||
* This interface extends [okhttp3.CookieJar] and adds methods to clear the cookies.
|
||||
*/
|
||||
interface ClearableCookieJar : CookieJar {
|
||||
|
||||
/**
|
||||
* Clear all the session cookies while maintaining the persisted ones.
|
||||
*/
|
||||
fun clearSession()
|
||||
|
||||
/**
|
||||
* Clear all the cookies from persistence and from the cache.
|
||||
*/
|
||||
fun clear()
|
||||
}
|
||||