Compare commits
1 Commits
v7.7.11
...
v0.3-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
|
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
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 a problem with a specific **manga source** or want to propose a new one, 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: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
|
||||||
required: true
|
|
||||||
- label: This is not an issue with a specific manga source. Otherwise, you have to open 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: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
|
|
||||||
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."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
15
.gitignore
vendored
@@ -3,27 +3,12 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
/.idea/caches
|
||||||
/.idea/libraries
|
/.idea/libraries
|
||||||
/.idea/dictionaries
|
|
||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
|
||||||
/.idea/discord.xml
|
|
||||||
/.idea/compiler.xml
|
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/ktlint-plugin.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
/.idea/kotlinScripting.xml
|
|
||||||
/.idea/kotlinc.xml
|
|
||||||
/.idea/deploymentTargetDropDown.xml
|
|
||||||
/.idea/androidTestResultsUserPreferences.xml
|
|
||||||
/.idea/deploymentTargetSelector.xml
|
|
||||||
/.idea/render.experimental.xml
|
|
||||||
/.idea/inspectionProfiles/
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
/.idea/deviceManager.xml
|
|
||||||
/.kotlin/
|
|
||||||
/.idea/AndroidProjectSystem.xml
|
|
||||||
|
|||||||
5
.idea/.gitignore
generated
vendored
@@ -1,5 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
/migrations.xml
|
|
||||||
/runConfigurations.xml
|
|
||||||
1
.idea/codeStyles/Project.xml
generated
@@ -23,7 +23,6 @@
|
|||||||
</option>
|
</option>
|
||||||
</AndroidXmlCodeStyleSettings>
|
</AndroidXmlCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="CMake">
|
<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">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
<option name="testRunner" value="PLATFORM" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="1.8" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveExternalAnnotations" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
<option name="useQualifiedModuleNames" value="true" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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="name" value="maven2" />
|
||||||
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
|
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
|
||||||
</remote-repository>
|
</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>
|
</component>
|
||||||
</project>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</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,12 +0,0 @@
|
|||||||
## Kotatsu contribution guidelines
|
|
||||||
|
|
||||||
+ If you want to **fix bugs** or **implement new features** that **already have an [issue card](https://github.com/KotatsuApp/Kotatsu/issues):** please assign this issue to you and/or comment about it.
|
|
||||||
+ If you want to **implement a new feature:** 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** might also 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.
|
|
||||||
53
LICENSE
@@ -619,3 +619,56 @@ Program, unless a warranty or assumption of liability accompanies a
|
|||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
65
README.md
@@ -1,57 +1,40 @@
|
|||||||
# Kotatsu
|
# Kotatsu
|
||||||
|
|
||||||
Kotatsu is a free and open-source manga reader for Android with built-in online content sources.
|
Kotatsu is a free and open source manga reader for Android.
|
||||||
|
|
||||||
[](https://github.com/KotatsuApp/kotatsu-parsers)   [](https://hosted.weblate.org/engage/kotatsu/) [](https://t.me/kotatsuapp) [](https://discord.gg/NNJ5RgVBC5) [](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
|
  [](https://travis-ci.org/nv95/Kotatsu)  [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
- **Recommended:** Download and install APK from **[GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. Application has a built-in self-updating feature.
|
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
|
||||||
- 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.
|
|
||||||
- Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
|
|
||||||
|
|
||||||
### Main Features
|
### Main Features
|
||||||
|
|
||||||
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
|
* Online manga catalogues
|
||||||
* Search manga by name, genres, and more filters
|
* Search manga by name and genre
|
||||||
* Reading history and bookmarks
|
* Reading history
|
||||||
* Favorites organized by user-defined categories
|
* Favourites with custom categories
|
||||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
* Saving manga and reading it offline
|
||||||
* Tablet-optimized Material You UI
|
* Tablet-optimized modern UI
|
||||||
* Standard and Webtoon-optimized customizable reader
|
* Reading third-party comics from CBZ
|
||||||
* Notifications about new chapters with updates feed
|
* Standard and Webtoon-optimized reader
|
||||||
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
|
* Notifications about new chapters
|
||||||
* Password/fingerprint-protected access to the app
|
|
||||||
|
|
||||||
### Screenshots
|
### 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
|
### 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
|
### Disclaimer
|
||||||
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
|
|
||||||
install instructions.
|
|
||||||
|
|
||||||
### DMCA disclaimer
|
The developers of this application does not have any affiliation with the content providers available.
|
||||||
|
|
||||||
The developers of this application do not have any affiliation with the content available in the app.
|
|
||||||
It collects content from sources that are freely available through any web browser
|
|
||||||
235
app/build.gradle
@@ -1,35 +1,39 @@
|
|||||||
import java.time.LocalDateTime
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
plugins {
|
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||||
id 'com.android.application'
|
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
|
||||||
id 'kotlin-android'
|
|
||||||
id 'kotlin-kapt'
|
|
||||||
id 'com.google.devtools.ksp'
|
|
||||||
id 'kotlin-parcelize'
|
|
||||||
id 'dagger.hilt.android.plugin'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 35
|
compileSdkVersion 29
|
||||||
buildToolsVersion = '35.0.0'
|
buildToolsVersion '29.0.3'
|
||||||
namespace = 'org.koitharu.kotatsu'
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdkVersion 16
|
||||||
targetSdk = 35
|
maxSdkVersion 20
|
||||||
versionCode = 703
|
targetSdkVersion 29
|
||||||
versionName = '7.7.11'
|
versionCode gitCommits
|
||||||
generatedDensities = []
|
versionName '0.3'
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
|
||||||
ksp {
|
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||||
arg('room.generateKotlin', 'true')
|
vectorDrawables.useSupportLibrary = true
|
||||||
arg('room.schemaLocation', "$projectDir/schemas")
|
kapt {
|
||||||
}
|
arguments {
|
||||||
androidResources {
|
arg('room.schemaLocation', "$projectDir/schemas".toString())
|
||||||
generateLocaleConfig true
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
archivesBaseName = "kotatsu_${gitCommits}"
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = '.debug'
|
applicationIdSuffix = '.debug'
|
||||||
@@ -39,158 +43,59 @@ android {
|
|||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
nightly {
|
|
||||||
initWith release
|
|
||||||
applicationIdSuffix = '.nightly'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
buildFeatures {
|
lintOptions {
|
||||||
viewBinding true
|
disable 'MissingTranslation'
|
||||||
buildConfig true
|
abortOnError false
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += [
|
|
||||||
'META-INF/README.md',
|
|
||||||
'META-INF/NOTICE.md'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceSets {
|
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
|
||||||
main.java.srcDirs += 'src/main/kotlin/'
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
||||||
freeCompilerArgs += [
|
|
||||||
'-opt-in=kotlin.ExperimentalStdlibApi',
|
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi',
|
|
||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
|
||||||
'-opt-in=coil3.annotation.ExperimentalCoilApi',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
lint {
|
|
||||||
abortOnError true
|
|
||||||
disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled', 'SimpleDateFormat'
|
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources true
|
unitTests.includeAndroidResources = true
|
||||||
unitTests.returnDefaultValues false
|
unitTests.returnDefaultValues = true
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applicationVariants.configureEach { variant ->
|
|
||||||
if (variant.name == 'nightly') {
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
def now = LocalDateTime.now()
|
|
||||||
output.versionCodeOverride = now.format("yyMMdd").toInteger()
|
|
||||||
output.versionNameOverride = 'N' + now.format("yyyyMMdd")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
afterEvaluate {
|
androidExtensions {
|
||||||
compileDebugKotlin {
|
experimental = true
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
def parsersVersion = libs.versions.parsers.get()
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
if (System.properties.containsKey('parsersVersionOverride')) {
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
// usage:
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||||
// -DparsersVersionOverride=$(curl -s https://api.github.com/repos/kotatsuapp/kotatsu-parsers/commits/master -H "Accept: application/vnd.github.sha" | cut -c -10)
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||||
parsersVersion = System.getProperty('parsersVersionOverride')
|
|
||||||
}
|
|
||||||
//noinspection UseTomlInstead
|
|
||||||
implementation("com.github.KotatsuApp:kotatsu-parsers:$parsersVersion") {
|
|
||||||
exclude group: 'org.json', module: 'json'
|
|
||||||
}
|
|
||||||
|
|
||||||
coreLibraryDesugaring libs.desugar.jdk.libs
|
implementation 'androidx.core:core-ktx:1.3.0-rc01'
|
||||||
implementation libs.kotlin.stdlib
|
implementation 'androidx.fragment:fragment-ktx:1.2.4'
|
||||||
implementation libs.kotlinx.coroutines.android
|
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
|
||||||
implementation libs.kotlinx.coroutines.guava
|
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 libs.androidx.appcompat
|
implementation 'androidx.room:room-runtime:2.2.5'
|
||||||
implementation libs.androidx.core
|
implementation 'androidx.room:room-ktx:2.2.5'
|
||||||
implementation libs.androidx.activity
|
kapt 'androidx.room:room-compiler:2.2.5'
|
||||||
implementation libs.androidx.fragment
|
|
||||||
implementation libs.androidx.transition
|
|
||||||
implementation libs.androidx.collection
|
|
||||||
implementation libs.lifecycle.viewmodel
|
|
||||||
implementation libs.lifecycle.service
|
|
||||||
implementation libs.lifecycle.process
|
|
||||||
implementation libs.androidx.constraintlayout
|
|
||||||
implementation libs.androidx.swiperefreshlayout
|
|
||||||
implementation libs.androidx.recyclerview
|
|
||||||
implementation libs.androidx.viewpager2
|
|
||||||
implementation libs.androidx.preference
|
|
||||||
implementation libs.androidx.biometric
|
|
||||||
implementation libs.material
|
|
||||||
implementation libs.androidx.lifecycle.common.java8
|
|
||||||
implementation libs.androidx.webkit
|
|
||||||
|
|
||||||
implementation libs.androidx.work.runtime
|
implementation 'com.github.moxy-community:moxy:2.1.2'
|
||||||
implementation libs.guava
|
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 libs.androidx.room.runtime
|
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||||
implementation libs.androidx.room.ktx
|
implementation 'com.squareup.okio:okio:2.5.0'
|
||||||
ksp libs.androidx.room.compiler
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
implementation libs.okhttp
|
implementation 'org.koin:koin-android:2.1.5'
|
||||||
implementation libs.okhttp.tls
|
implementation 'io.coil-kt:coil:0.9.5'
|
||||||
implementation libs.okhttp.dnsoverhttps
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||||
implementation libs.okio
|
implementation 'com.tomclaw.cache:cache:1.0'
|
||||||
|
|
||||||
implementation libs.adapterdelegates
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||||
implementation libs.adapterdelegates.viewbinding
|
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
|
||||||
|
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
|
||||||
|
|
||||||
implementation libs.hilt.android
|
testImplementation 'junit:junit:4.13'
|
||||||
kapt libs.hilt.compiler
|
testImplementation 'org.json:json:20190722'
|
||||||
implementation libs.androidx.hilt.work
|
}
|
||||||
kapt libs.androidx.hilt.compiler
|
|
||||||
|
|
||||||
implementation libs.coil.core
|
|
||||||
implementation libs.coil.network
|
|
||||||
implementation libs.coil.gif
|
|
||||||
implementation libs.coil.svg
|
|
||||||
implementation libs.avif.decoder
|
|
||||||
implementation libs.ssiv
|
|
||||||
implementation libs.disk.lru.cache
|
|
||||||
implementation libs.markwon
|
|
||||||
|
|
||||||
implementation libs.acra.http
|
|
||||||
implementation libs.acra.dialog
|
|
||||||
|
|
||||||
implementation libs.conscrypt.android
|
|
||||||
|
|
||||||
debugImplementation libs.leakcanary.android
|
|
||||||
debugImplementation libs.workinspector
|
|
||||||
|
|
||||||
testImplementation libs.junit
|
|
||||||
testImplementation libs.json
|
|
||||||
testImplementation libs.kotlinx.coroutines.test
|
|
||||||
|
|
||||||
androidTestImplementation libs.androidx.runner
|
|
||||||
androidTestImplementation libs.androidx.rules
|
|
||||||
androidTestImplementation libs.androidx.test.core
|
|
||||||
androidTestImplementation libs.androidx.junit
|
|
||||||
|
|
||||||
androidTestImplementation libs.kotlinx.coroutines.test
|
|
||||||
|
|
||||||
androidTestImplementation libs.androidx.room.testing
|
|
||||||
androidTestImplementation libs.moshi.kotlin
|
|
||||||
|
|
||||||
androidTestImplementation libs.hilt.android.testing
|
|
||||||
kaptAndroidTest libs.hilt.android.compiler
|
|
||||||
}
|
|
||||||
24
app/proguard-rules.pro
vendored
@@ -1,4 +1,3 @@
|
|||||||
-optimizationpasses 8
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
public static void checkExpressionValueIsNotNull(...);
|
public static void checkExpressionValueIsNotNull(...);
|
||||||
@@ -6,25 +5,8 @@
|
|||||||
public static void checkReturnedValueIsNotNull(...);
|
public static void checkReturnedValueIsNotNull(...);
|
||||||
public static void checkFieldIsNotNull(...);
|
public static void checkFieldIsNotNull(...);
|
||||||
public static void checkParameterIsNotNull(...);
|
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.* { *; }
|
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||||
-dontwarn okhttp3.internal.platform.**
|
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
|
||||||
-dontwarn org.conscrypt.**
|
public <init>(...);
|
||||||
-dontwarn org.bouncycastle.**
|
}
|
||||||
-dontwarn org.openjsse.**
|
|
||||||
-dontwarn com.google.j2objc.annotations.**
|
|
||||||
-dontwarn coil3.PlatformContext
|
|
||||||
|
|
||||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
|
||||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
|
||||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
|
||||||
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
|
|
||||||
-keep class org.jsoup.parser.Tag
|
|
||||||
-keep class org.jsoup.internal.StringUtil
|
|
||||||
|
|
||||||
-keep class org.acra.security.NoKeyStoreFactory { *; }
|
|
||||||
-keep class org.acra.config.DefaultRetryPolicy { *; }
|
|
||||||
-keep class org.acra.attachment.DefaultAttachmentProvider { *; }
|
|
||||||
-keep class org.acra.sender.JobSenderService
|
|
||||||
@@ -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,79 +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,
|
|
||||||
force = false,
|
|
||||||
)
|
|
||||||
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,112 +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.assertEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
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.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,
|
|
||||||
force = false,
|
|
||||||
)
|
|
||||||
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.getTagsDao().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,73 +0,0 @@
|
|||||||
package org.koitharu.kotatsu
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
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
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
|
||||||
|
|
||||||
class KotatsuApp : BaseApp() {
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
|
||||||
super.attachBaseContext(base)
|
|
||||||
enableStrictMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableStrictMode() {
|
|
||||||
val notifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
StrictModeNotifier(this)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
StrictMode.setThreadPolicy(
|
|
||||||
StrictMode.ThreadPolicy.Builder().apply {
|
|
||||||
detectNetwork()
|
|
||||||
detectDiskWrites()
|
|
||||||
detectCustomSlowCalls()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc()
|
|
||||||
penaltyLog()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
|
|
||||||
penaltyListener(notifier.executor, notifier)
|
|
||||||
}
|
|
||||||
}.build(),
|
|
||||||
)
|
|
||||||
StrictMode.setVmPolicy(
|
|
||||||
StrictMode.VmPolicy.Builder().apply {
|
|
||||||
detectActivityLeaks()
|
|
||||||
detectLeakedSqlLiteObjects()
|
|
||||||
detectLeakedClosableObjects()
|
|
||||||
detectLeakedRegistrationObjects()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectContentUriWithoutPermission()
|
|
||||||
detectFileUriExposure()
|
|
||||||
setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
|
||||||
setClassInstanceLimit(PagesCache::class.java, 1)
|
|
||||||
setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
|
||||||
setClassInstanceLimit(PageLoader::class.java, 1)
|
|
||||||
setClassInstanceLimit(ReaderViewModel::class.java, 1)
|
|
||||||
penaltyLog()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
|
|
||||||
penaltyListener(notifier.executor, notifier)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
)
|
|
||||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply {
|
|
||||||
detectWrongFragmentContainer()
|
|
||||||
detectFragmentTagUsage()
|
|
||||||
detectRetainInstanceUsage()
|
|
||||||
detectSetUserVisibleHint()
|
|
||||||
detectWrongNestedHierarchy()
|
|
||||||
detectFragmentReuse()
|
|
||||||
penaltyLog()
|
|
||||||
if (notifier != null) {
|
|
||||||
penaltyListener(notifier)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package org.koitharu.kotatsu
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.Notification.BigTextStyle
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.StrictMode
|
|
||||||
import android.os.strictmode.Violation
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.PendingIntentCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.asExecutor
|
|
||||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import androidx.fragment.app.strictmode.Violation as FragmentViolation
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
|
||||||
class StrictModeNotifier(
|
|
||||||
private val context: Context,
|
|
||||||
) : StrictMode.OnVmViolationListener, StrictMode.OnThreadViolationListener, FragmentStrictMode.OnViolationListener {
|
|
||||||
|
|
||||||
val executor = Dispatchers.Default.asExecutor()
|
|
||||||
|
|
||||||
private val notificationManager by lazy {
|
|
||||||
val nm = checkNotNull(context.getSystemService<NotificationManager>())
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.strict_mode),
|
|
||||||
NotificationManager.IMPORTANCE_LOW,
|
|
||||||
)
|
|
||||||
nm.createNotificationChannel(channel)
|
|
||||||
nm
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVmViolation(v: Violation) = showNotification(v)
|
|
||||||
|
|
||||||
override fun onThreadViolation(v: Violation) = showNotification(v)
|
|
||||||
|
|
||||||
override fun onViolation(violation: FragmentViolation) = showNotification(violation)
|
|
||||||
|
|
||||||
private fun showNotification(violation: Throwable) = Notification.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_bug)
|
|
||||||
.setContentTitle(context.getString(R.string.strict_mode))
|
|
||||||
.setContentText(violation.message)
|
|
||||||
.setStyle(
|
|
||||||
BigTextStyle()
|
|
||||||
.setBigContentTitle(context.getString(R.string.strict_mode))
|
|
||||||
.setSummaryText(violation.message)
|
|
||||||
.bigText(violation.stackTraceToString()),
|
|
||||||
).setShowWhen(true)
|
|
||||||
.setContentIntent(
|
|
||||||
PendingIntentCompat.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
ShareHelper(context).getShareTextIntent(violation.stackTraceToString()),
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setGroup(CHANNEL_ID)
|
|
||||||
.build()
|
|
||||||
.let { notificationManager.notify(CHANNEL_ID, violation.hashCode().absoluteValue, it) }
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
const val CHANNEL_ID = "strict_mode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okio.Buffer
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders.ACCEPT_ENCODING
|
|
||||||
|
|
||||||
class CurlLoggingInterceptor(
|
|
||||||
private val curlOptions: String? = null
|
|
||||||
) : Interceptor {
|
|
||||||
|
|
||||||
private val escapeRegex = Regex("([\\[\\]\"])")
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()).also {
|
|
||||||
logRequest(it.networkResponse?.request ?: it.request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logRequest(request: 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.toString().escape()).append('"')
|
|
||||||
|
|
||||||
log("---cURL (" + request.url + ")")
|
|
||||||
log(curlCmd.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.escape() = replace(escapeRegex) { match ->
|
|
||||||
"\\" + match.value
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun log(msg: String) {
|
|
||||||
Log.d("CURL", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
|
|
||||||
fun assertNotInMainThread() = check(Looper.myLooper() != Looper.getMainLooper()) {
|
|
||||||
"Calling this from the main thread is prohibited"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import leakcanary.LeakCanary
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.workinspector.WorkInspector
|
|
||||||
|
|
||||||
class SettingsMenuProvider(
|
|
||||||
private val context: Context,
|
|
||||||
) : MenuProvider {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.opt_settings, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
|
||||||
R.id.action_leaks -> {
|
|
||||||
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_works -> {
|
|
||||||
context.startActivity(WorkInspector.getIntent(context))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="#FFFFFF">
|
|
||||||
<group android:scaleX="0.98150784"
|
|
||||||
android:scaleY="0.98150784"
|
|
||||||
android:translateX="0.22190611"
|
|
||||||
android:translateY="-0.2688478">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
Before Width: | Height: | Size: 417 B |
|
Before Width: | Height: | Size: 308 B |
|
Before Width: | Height: | Size: 480 B |
|
Before Width: | Height: | Size: 792 B |
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,16 +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" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@id/action_works"
|
|
||||||
android:title="@string/wi_lib_name"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
</adaptive-icon>
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
</adaptive-icon>
|
||||||
</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,5 +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>
|
|
||||||
<bool name="wi_launcher_icon_enabled" 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,4 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">Kotatsu Dev</string>
|
|
||||||
<string name="strict_mode">Strict mode</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
|
||||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
|
||||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
|
||||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
|
||||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
|
||||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
|
||||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
|
||||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
|
||||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
|
||||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
|
||||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
|
||||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
|
||||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
|
||||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
|
||||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
|
||||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
|
||||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
|
||||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
|
||||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
|
||||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
|
||||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
|
||||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
|
||||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
|
||||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
|
||||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
|
||||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
|
||||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
|
||||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
|
||||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
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.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = TABLE_FAVOURITES,
|
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||||
primaryKeys = ["manga_id", "category_id"],
|
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = MangaEntity::class,
|
entity = MangaEntity::class,
|
||||||
parentColumns = ["manga_id"],
|
parentColumns = ["manga_id"],
|
||||||
@@ -27,7 +23,5 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|||||||
data class FavouriteEntity(
|
data class FavouriteEntity(
|
||||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||||
@ColumnInfo(name = "category_id", index = true) val categoryId: 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 = "created_at") val createdAt: Long,
|
)
|
||||||
@ColumnInfo(name = "deleted_at") val deletedAt: 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.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
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,
|
@Embedded val favourite: FavouriteEntity,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "manga_id",
|
parentColumn = "manga_id",
|
||||||
@@ -1,32 +1,37 @@
|
|||||||
package org.koitharu.kotatsu.history.data
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import java.util.*
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = TABLE_HISTORY,
|
tableName = "history", foreignKeys = [
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = MangaEntity::class,
|
entity = MangaEntity::class,
|
||||||
parentColumns = ["manga_id"],
|
parentColumns = ["manga_id"],
|
||||||
childColumns = ["manga_id"],
|
childColumns = ["manga_id"],
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE
|
||||||
),
|
)
|
||||||
],
|
]
|
||||||
)
|
)
|
||||||
data class HistoryEntity(
|
data class HistoryEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
@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 = "updated_at") val updatedAt: Long,
|
||||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||||
@ColumnInfo(name = "page") val page: Int,
|
@ColumnInfo(name = "page") val page: Int,
|
||||||
@ColumnInfo(name = "scroll") val scroll: Float,
|
@ColumnInfo(name = "scroll") val scroll: Float
|
||||||
@ColumnInfo(name = "percent") val percent: Float,
|
) {
|
||||||
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
|
|
||||||
@ColumnInfo(name = "chapters") val chaptersCount: Int,
|
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.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
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,
|
@Embedded val history: HistoryEntity,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "manga_id",
|
parentColumn = "manga_id",
|
||||||
@@ -19,5 +16,5 @@ class HistoryWithManga(
|
|||||||
entityColumn = "tag_id",
|
entityColumn = "tag_id",
|
||||||
associateBy = Junction(MangaTagsEntity::class)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
package org.koitharu.kotatsu.local.data.index
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "local_index",
|
tableName = "preferences", foreignKeys = [
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = MangaEntity::class,
|
entity = MangaEntity::class,
|
||||||
parentColumns = ["manga_id"],
|
parentColumns = ["manga_id"],
|
||||||
childColumns = ["manga_id"],
|
childColumns = ["manga_id"],
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE
|
||||||
),
|
)]
|
||||||
],
|
|
||||||
)
|
)
|
||||||
class LocalMangaIndexEntity(
|
data class MangaPrefsEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||||
@ColumnInfo(name = "path") val path: String,
|
@ColumnInfo(name = "mode") val mode: Int
|
||||||
)
|
)
|
||||||
@@ -3,27 +3,24 @@ package org.koitharu.kotatsu.core.db.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = TABLE_MANGA_TAGS,
|
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
|
||||||
primaryKeys = ["manga_id", "tag_id"],
|
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = MangaEntity::class,
|
entity = MangaEntity::class,
|
||||||
parentColumns = ["manga_id"],
|
parentColumns = ["manga_id"],
|
||||||
childColumns = ["manga_id"],
|
childColumns = ["manga_id"],
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE
|
||||||
),
|
),
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = TagEntity::class,
|
entity = TagEntity::class,
|
||||||
parentColumns = ["tag_id"],
|
parentColumns = ["tag_id"],
|
||||||
childColumns = ["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 = "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",
|
entityColumn = "tag_id",
|
||||||
associateBy = Junction(MangaTagsEntity::class)
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 = "tracks", foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = MangaEntity::class,
|
||||||
|
parentColumns = ["manga_id"],
|
||||||
|
childColumns = ["manga_id"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class TrackEntity (
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||||
|
@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,
|
||||||
|
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
object Migration1To2 : Migration(1, 2) {
|
||||||
|
/**
|
||||||
|
* Adding foreign keys
|
||||||
|
*/
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
/* manga_tags */
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
|
||||||
|
"PRIMARY KEY(manga_id, tag_id), " +
|
||||||
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
|
||||||
|
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
|
)
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
|
||||||
|
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
|
||||||
|
database.execSQL("DROP TABLE manga_tags")
|
||||||
|
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
|
||||||
|
/* favourites */
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
|
||||||
|
"PRIMARY KEY(manga_id, category_id), " +
|
||||||
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
|
||||||
|
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
|
)
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
|
||||||
|
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
|
||||||
|
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
|
||||||
|
database.execSQL("DROP TABLE favourites")
|
||||||
|
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
|
||||||
|
/* history */
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
|
||||||
|
"PRIMARY KEY(manga_id), " +
|
||||||
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
|
)
|
||||||
|
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
|
||||||
|
database.execSQL("DROP TABLE history")
|
||||||
|
database.execSQL("ALTER TABLE history_tmp RENAME TO history")
|
||||||
|
/* preferences */
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
|
||||||
|
" PRIMARY KEY(manga_id), " +
|
||||||
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
|
)
|
||||||
|
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
|
||||||
|
database.execSQL("DROP TABLE preferences")
|
||||||
|
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
object Migration2To3 : Migration(2, 3) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
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
|
||||||