Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cd156ae42 | ||
|
|
4a22f62ec5 | ||
|
|
9ad37b4412 | ||
|
|
9fcabcb05e | ||
|
|
db1ae6020d | ||
|
|
7e039c9055 | ||
|
|
db7482ff12 | ||
|
|
d63ae7cadd | ||
|
|
c4fa0d405e | ||
|
|
63fef3ab7c | ||
|
|
7019c07a6d | ||
|
|
006ea9844a | ||
|
|
d2bbfe01f1 | ||
|
|
86d8ff3c68 | ||
|
|
00dacc32df | ||
|
|
1c1bd9265e | ||
|
|
0bb090eee6 | ||
|
|
d2f3bfb2e3 | ||
|
|
634ce0dddf | ||
|
|
c82bacb037 | ||
|
|
3edfd0892a | ||
|
|
30c0fd600f | ||
|
|
ccb31de1ba | ||
|
|
a74b623c10 | ||
|
|
5808e8f321 | ||
|
|
4f3fef3bfe | ||
|
|
0c07e649bf |
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -44,7 +44,7 @@ body:
|
||||
label: Kotatsu version
|
||||
description: You can find your Kotatsu version in **Settings → About**.
|
||||
placeholder: |
|
||||
Example: "3.2.3"
|
||||
Example: "3.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -87,7 +87,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
|
||||
required: true
|
||||
- label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
|
||||
- label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
|
||||
required: true
|
||||
- label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
|
||||
- label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/kotlinScripting.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/androidTestResultsUserPreferences.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -7,7 +7,7 @@
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="Android Studio default JDK" />
|
||||
<option name="gradleJvm" value="Embedded JDK" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
287
.idea/icon.svg
generated
Normal file
287
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,287 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 13 KiB |
29
README.md
29
README.md
@@ -1,8 +1,8 @@
|
||||
# Kotatsu
|
||||
# Kotatsu
|
||||
|
||||
Kotatsu is a free and open source manga reader for Android.
|
||||
|
||||
   [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
|
||||
   [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
|
||||
|
||||
### Download
|
||||
|
||||
@@ -12,14 +12,13 @@ height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
|
||||
|
||||
Download APK from Github Releases:
|
||||
|
||||
- [Latest release](https://github.com/nv95/Kotatsu/releases/latest)
|
||||
- [Legacy build](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy) (with Android 4.1+ support)
|
||||
- [Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest)
|
||||
|
||||
### Main Features
|
||||
|
||||
* Online manga catalogues
|
||||
* Search manga by name and genre
|
||||
* Reading history
|
||||
* Search manga by name and genres
|
||||
* Reading history and bookmarks
|
||||
* Favourites organized by user-defined categories
|
||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
||||
* Tablet-optimized material design UI
|
||||
@@ -30,12 +29,12 @@ Download APK from Github Releases:
|
||||
|
||||
### Screenshots
|
||||
|
||||
|  |  |  |
|
||||
|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
|  |  |
|
||||
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||
|  |  |
|
||||
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
|
||||
### Localization
|
||||
|
||||
@@ -43,16 +42,18 @@ Download APK from Github Releases:
|
||||
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
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 <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
|
||||
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 <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
|
||||
|
||||
### 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.
|
||||
(at your option) any later version.
|
||||
|
||||
### Disclaimer
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 32
|
||||
versionCode 409
|
||||
versionName '3.3'
|
||||
versionCode 410
|
||||
versionName '3.3.1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -64,38 +64,43 @@ android {
|
||||
unitTests.returnDefaultValues = false
|
||||
}
|
||||
}
|
||||
afterEvaluate {
|
||||
compileDebugKotlin {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
||||
}
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
implementation('com.github.nv95:kotatsu-parsers:f46c5add46') {
|
||||
implementation('com.github.nv95:kotatsu-parsers:8a3b6df91d') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.5.0-rc01'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.0-rc01'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.0-rc01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha01'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha02'
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0-rc01'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.4.2'
|
||||
implementation 'androidx.room:room-ktx:2.4.2'
|
||||
kapt 'androidx.room:room-compiler:2.4.2'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
||||
implementation 'com.squareup.okio:okio:3.1.0'
|
||||
|
||||
@@ -107,15 +112,24 @@ dependencies {
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
|
||||
implementation 'ch.acra:acra-mail:5.9.3'
|
||||
implementation 'ch.acra:acra-dialog:5.9.3'
|
||||
|
||||
debugImplementation 'org.jsoup:jsoup:1.15.1'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
|
||||
testImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'
|
||||
androidTestImplementation 'io.insert-koin:koin-test:3.2.0'
|
||||
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.4.2'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
|
||||
}
|
||||
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
36
app/src/androidTest/assets/manga/empty.json
Normal file
36
app/src/androidTest/assets/manga/empty.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
163
app/src/androidTest/assets/manga/full.json
Normal file
163
app/src/androidTest/assets/manga/full.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"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,14 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import java.io.IOException
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koitharu.kotatsu.core.db.migrations.*
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MangaDatabaseTest {
|
||||
@@ -16,8 +15,7 @@ class MangaDatabaseTest {
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
MangaDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
MangaDatabase::class.java,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -37,7 +35,6 @@ class MangaDatabaseTest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TEST_DB = "test-db"
|
||||
@@ -50,6 +47,9 @@ class MangaDatabaseTest {
|
||||
Migration5To6(),
|
||||
Migration6To7(),
|
||||
Migration7To8(),
|
||||
Migration8To9(),
|
||||
Migration9To10(),
|
||||
Migration10To11(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package org.koitharu.kotatsu.tracker.domain
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.inject
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TrackerTest : KoinTest {
|
||||
|
||||
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
private val mangaAdapter = moshi.adapter(Manga::class.java)
|
||||
private val historyRegistry by inject<HistoryRepository>()
|
||||
private val repository by inject<TrackingRepository>()
|
||||
private val dataRepository by inject<MangaDataRepository>()
|
||||
private val tracker by inject<Tracker>()
|
||||
|
||||
@Test
|
||||
fun noUpdates() = runTest {
|
||||
val manga = loadManga("full.json")
|
||||
tracker.deleteTrack(manga.id)
|
||||
|
||||
tracker.checkUpdates(manga, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||
tracker.checkUpdates(manga, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasUpdates() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaFull = loadManga("full.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badIds() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaBad = loadManga("bad_ids.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun badIds2() = runTest {
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaBad = loadManga("bad_ids.json")
|
||||
val mangaFull = loadManga("full.json")
|
||||
tracker.deleteTrack(mangaFirst.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullReset() = runTest {
|
||||
val mangaFull = loadManga("full.json")
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
val mangaEmpty = loadManga("empty.json")
|
||||
tracker.deleteTrack(mangaFull.id)
|
||||
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncWithHistory() = runTest {
|
||||
val mangaFull = loadManga("full.json")
|
||||
val mangaFirst = loadManga("first_chapters.json")
|
||||
tracker.deleteTrack(mangaFull.id)
|
||||
|
||||
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||
assertFalse(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assertEquals(3, newChapters.size)
|
||||
}
|
||||
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||
|
||||
val chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
||||
repository.syncWithHistory(mangaFull, chapter.id)
|
||||
|
||||
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||
assertTrue(isValid)
|
||||
assert(newChapters.isEmpty())
|
||||
}
|
||||
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||
}
|
||||
|
||||
private suspend fun loadManga(name: String): Manga {
|
||||
val assets = InstrumentationRegistry.getInstrumentation().context.assets
|
||||
val manga = assets.open("manga/$name").use {
|
||||
mangaAdapter.fromJson(it.source().buffer())
|
||||
} ?: throw RuntimeException("Cannot read manga from json \"$name\"")
|
||||
dataRepository.storeManga(manga)
|
||||
return manga
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder?
|
||||
sortOrder: SortOrder,
|
||||
): List<Manga> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<application
|
||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||
@@ -67,11 +68,6 @@
|
||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
||||
android:label="@string/error_occurred"
|
||||
android:theme="@android:style/Theme.DeviceDefault"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
||||
android:label="@string/favourites_categories"
|
||||
@@ -85,9 +81,6 @@
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
||||
android:label="@string/search" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
|
||||
android:label="@string/search" />
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.StrictMode
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
@@ -13,7 +19,6 @@ import org.koitharu.kotatsu.core.db.databaseModule
|
||||
import org.koitharu.kotatsu.core.github.githubModule
|
||||
import org.koitharu.kotatsu.core.network.networkModule
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
||||
import org.koitharu.kotatsu.core.ui.uiModule
|
||||
import org.koitharu.kotatsu.details.detailsModule
|
||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
||||
@@ -41,7 +46,6 @@ class KotatsuApp : Application() {
|
||||
enableStrictMode()
|
||||
}
|
||||
initKoin()
|
||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
||||
registerActivityLifecycleCallbacks(get<ActivityRecreationHandle>())
|
||||
@@ -75,6 +79,36 @@ class KotatsuApp : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.KEY_VALUE_LIST
|
||||
reportContent = listOf(
|
||||
ReportField.PACKAGE_NAME,
|
||||
ReportField.APP_VERSION_CODE,
|
||||
ReportField.APP_VERSION_NAME,
|
||||
ReportField.ANDROID_VERSION,
|
||||
ReportField.PHONE_MODEL,
|
||||
ReportField.CRASH_CONFIGURATION,
|
||||
ReportField.STACK_TRACE,
|
||||
ReportField.SHARED_PREFERENCES,
|
||||
)
|
||||
dialog {
|
||||
text = getString(R.string.crash_text)
|
||||
title = getString(R.string.error_occurred)
|
||||
positiveButtonText = getString(R.string.send)
|
||||
resIcon = R.drawable.ic_alert_outline
|
||||
resTheme = android.R.style.Theme_Material_Light_Dialog_Alert
|
||||
}
|
||||
mailSender {
|
||||
mailTo = getString(R.string.email_error_report)
|
||||
reportAsFile = true
|
||||
reportFileName = "stacktrace.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -83,8 +82,9 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||
ActivityCompat.recreate(this)
|
||||
return true
|
||||
// ActivityCompat.recreate(this)
|
||||
throw RuntimeException("Test crash")
|
||||
// return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||
requireContext().displayCompat?.let {
|
||||
val metrics = DisplayMetrics()
|
||||
it.getRealMetrics(metrics)
|
||||
behavior?.peekHeight = metrics.heightPixels / 2
|
||||
behavior?.peekHeight = (metrics.heightPixels * 0.4).toInt()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
||||
@@ -16,10 +16,7 @@ class WindowInsetsDelegate(
|
||||
|
||||
private var lastInsets: Insets? = null
|
||||
|
||||
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? {
|
||||
if (insets == null) {
|
||||
return null
|
||||
}
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets
|
||||
val newInsets = if (handleImeInsets) {
|
||||
Insets.max(
|
||||
@@ -49,7 +46,7 @@ class WindowInsetsDelegate(
|
||||
) {
|
||||
view.removeOnLayoutChangeListener(this)
|
||||
if (lastInsets == null) { // Listener may not be called
|
||||
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view))
|
||||
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view) ?: return)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ import org.koitharu.kotatsu.history.data.HistoryDao
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
|
||||
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
|
||||
|
||||
@Dao
|
||||
interface TrackLogsDao {
|
||||
@@ -21,7 +21,7 @@ interface TrackLogsDao {
|
||||
suspend fun removeAll(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
|
||||
suspend fun cleanup()
|
||||
suspend fun gc()
|
||||
|
||||
@Query("SELECT COUNT(*) FROM track_logs")
|
||||
suspend fun count(): Int
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
@@ -35,13 +33,6 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
|
||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
||||
|
||||
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
|
||||
id = trackLog.id,
|
||||
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
||||
manga = manga.toManga(tags.toMangaTags()),
|
||||
createdAt = Date(trackLog.createdAt)
|
||||
)
|
||||
|
||||
// Model to entity
|
||||
|
||||
fun Manga.toEntity() = MangaEntity(
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
|
||||
class CompositeException(val errors: Collection<Throwable>) : Exception() {
|
||||
|
||||
override val message: String = errors.mapNotNullToSet { it.message }.joinToString()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.util.*
|
||||
|
||||
fun MangaSource.getLocaleTitle(): String? {
|
||||
val lc = Locale(locale ?: return null)
|
||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.*
|
||||
|
||||
data class MangaTracking(
|
||||
val manga: Manga,
|
||||
val knownChaptersCount: Int,
|
||||
val lastChapterId: Long,
|
||||
val lastNotifiedChapterId: Long,
|
||||
val lastCheck: Date?
|
||||
)
|
||||
@@ -13,12 +13,9 @@ interface MangaRepository {
|
||||
|
||||
val sortOrders: Set<SortOrder>
|
||||
|
||||
suspend fun getList(
|
||||
offset: Int,
|
||||
query: String? = null,
|
||||
tags: Set<MangaTag>? = null,
|
||||
sortOrder: SortOrder? = null,
|
||||
): List<Manga>
|
||||
suspend fun getList(offset: Int, query: String): List<Manga>
|
||||
|
||||
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga>
|
||||
|
||||
suspend fun getDetails(manga: Manga): Manga
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
||||
getConfig().defaultSortOrder = value
|
||||
}
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder?,
|
||||
): List<Manga> = parser.getList(offset, query, tags, sortOrder)
|
||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
return parser.getList(offset, query)
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
return parser.getList(offset, tags, sortOrder)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga)
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||
val intent = CrashActivity.newIntent(applicationContext, e)
|
||||
intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
applicationContext.startActivity(intent)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTraceDebug()
|
||||
}
|
||||
Log.e("CRASH", e.message, e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ActivityCrashBinding
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
|
||||
class CrashActivity : Activity(), View.OnClickListener {
|
||||
|
||||
private lateinit var binding: ActivityCrashBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.textView.text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
binding.buttonClose.setOnClickListener(this)
|
||||
binding.buttonRestart.setOnClickListener(this)
|
||||
binding.buttonReport.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_crash, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_share -> {
|
||||
ShareHelper(this).shareText(binding.textView.text.toString())
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_close -> {
|
||||
finish()
|
||||
}
|
||||
R.id.button_restart -> {
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
R.id.button_report -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse("https://github.com/nv95/Kotatsu/issues")
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.report_github)))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MAX_TRACE_SIZE = 131071
|
||||
|
||||
fun newIntent(context: Context, error: Throwable): Intent {
|
||||
val crashInfo = error
|
||||
.stackTraceToString()
|
||||
.trimIndent()
|
||||
.ellipsize(MAX_TRACE_SIZE)
|
||||
val intent = Intent(context, CrashActivity::class.java)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, crashInfo)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -27,6 +28,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ChaptersFragment :
|
||||
@@ -43,11 +45,6 @@ class ChaptersFragment :
|
||||
private var actionMode: ActionMode? = null
|
||||
private var selectionDecoration: ChaptersSelectionDecoration? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
@@ -72,6 +69,7 @@ class ChaptersFragment :
|
||||
binding.textViewHolder.isVisible = it
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
addMenuProvider(ChaptersMenuProvider())
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -81,31 +79,6 @@ class ChaptersFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_chapters, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
|
||||
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_reversed -> {
|
||||
viewModel.setChaptersReversed(!item.isChecked)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||
selectionDecoration?.toggleItemChecked(item.chapter.id)
|
||||
@@ -268,4 +241,30 @@ class ChaptersFragment :
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
private inner class ChaptersMenuProvider : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_chapters, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this@ChaptersFragment)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this@ChaptersFragment)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
|
||||
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_reversed -> {
|
||||
viewModel.setChaptersReversed(!menuItem.isChecked)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import android.widget.Toast
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -45,7 +43,6 @@ import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
class DetailsActivity :
|
||||
@@ -166,16 +163,6 @@ class DetailsActivity :
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_share -> {
|
||||
viewModel.manga.value?.let {
|
||||
if (it.source == MangaSource.LOCAL) {
|
||||
ShareHelper(this).shareCbz(listOf(it.url.toUri().toFile()))
|
||||
} else {
|
||||
ShareHelper(this).shareMangaLink(it)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
val title = viewModel.manga.value?.title.orEmpty()
|
||||
MaterialAlertDialogBuilder(this)
|
||||
|
||||
@@ -8,8 +8,10 @@ import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -40,6 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
class DetailsFragment :
|
||||
@@ -52,11 +55,6 @@ class DetailsFragment :
|
||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -76,11 +74,7 @@ class DetailsFragment :
|
||||
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_details_info, menu)
|
||||
addMenuProvider(DetailsMenuProvider())
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
@@ -329,4 +323,26 @@ class DetailsFragment :
|
||||
} ?: request.fallback(R.drawable.ic_placeholder)
|
||||
request.enqueueWith(coil)
|
||||
}
|
||||
|
||||
private inner class DetailsMenuProvider : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_details_info, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_share -> {
|
||||
viewModel.manga.value?.let {
|
||||
val context = requireContext()
|
||||
if (it.source == MangaSource.LOCAL) {
|
||||
ShareHelper(context).shareCbz(listOf(it.url.toUri().toFile()))
|
||||
} else {
|
||||
ShareHelper(context).shareMangaLink(it)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.*
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||
@@ -23,14 +30,13 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import java.io.IOException
|
||||
|
||||
class DetailsViewModel(
|
||||
intent: MangaIntent,
|
||||
private val historyRepository: HistoryRepository,
|
||||
favouritesRepository: FavouritesRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
trackingRepository: TrackingRepository,
|
||||
mangaDataRepository: MangaDataRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
@@ -54,9 +60,8 @@ class DetailsViewModel(
|
||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
private val newChapters = viewModelScope.async(Dispatchers.Default) {
|
||||
trackingRepository.getNewChaptersCount(delegate.mangaId)
|
||||
}
|
||||
private val newChapters = trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
|
||||
@@ -65,7 +70,7 @@ class DetailsViewModel(
|
||||
|
||||
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
||||
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
||||
val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) }
|
||||
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
|
||||
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
|
||||
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
@@ -87,9 +92,12 @@ class DetailsViewModel(
|
||||
branches.indexOf(selected)
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val isChaptersEmpty: LiveData<Boolean> = delegate.manga.map { m ->
|
||||
m != null && m.chapters.isNullOrEmpty()
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
val isChaptersEmpty: LiveData<Boolean> = combine(
|
||||
delegate.manga,
|
||||
isLoading.asFlow(),
|
||||
) { m, loading ->
|
||||
m != null && m.chapters.isNullOrEmpty() && !loading
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext, false)
|
||||
|
||||
val chapters = combine(
|
||||
combine(
|
||||
@@ -97,8 +105,9 @@ class DetailsViewModel(
|
||||
delegate.relatedManga,
|
||||
history,
|
||||
delegate.selectedBranch,
|
||||
) { manga, related, history, branch ->
|
||||
delegate.mapChapters(manga, related, history, newChapters.await(), branch)
|
||||
newChapters,
|
||||
) { manga, related, history, branch, news ->
|
||||
delegate.mapChapters(manga, related, history, news, branch)
|
||||
},
|
||||
chaptersReversed,
|
||||
chaptersQuery,
|
||||
|
||||
@@ -11,8 +11,8 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.google.android.material.R as materialR
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.CrashActivity
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||
import org.koitharu.kotatsu.download.ui.DownloadsActivity
|
||||
@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.utils.PendingIntentCompat
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class DownloadNotification(private val context: Context, startId: Int) {
|
||||
|
||||
@@ -92,14 +91,6 @@ class DownloadNotification(private val context: Context, startId: Int) {
|
||||
builder.setContentText(message)
|
||||
builder.setAutoCancel(true)
|
||||
builder.setOngoing(false)
|
||||
builder.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
state.manga.hashCode(),
|
||||
CrashActivity.newIntent(context, state.error),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
builder.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.koitharu.kotatsu.favourites.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
@@ -19,12 +21,12 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeListener
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
@@ -43,11 +45,6 @@ class FavouritesContainerFragment :
|
||||
private var pagerAdapter: FavouritesPagerAdapter? = null
|
||||
private var stubBinding: ItemEmptyStateBinding? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
@@ -61,6 +58,7 @@ class FavouritesContainerFragment :
|
||||
pagerAdapter = adapter
|
||||
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
|
||||
actionModeDelegate.addListener(this, viewLifecycleOwner)
|
||||
addMenuProvider(FavouritesContainerMenuProvider(view.context))
|
||||
|
||||
viewModel.visibleCategories.observe(viewLifecycleOwner, ::onCategoriesChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
@@ -115,21 +113,6 @@ class FavouritesContainerFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_favourites, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_categories -> {
|
||||
context?.let {
|
||||
startActivity(CategoriesActivity.newIntent(it))
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.favourites.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
|
||||
|
||||
class FavouritesContainerMenuProvider(
|
||||
private val context: Context,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_favourites, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_categories -> {
|
||||
context.startActivity(CategoriesActivity.newIntent(context))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,15 @@ package org.koitharu.kotatsu.favourites.ui.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.iterator
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.titleRes
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class FavouritesListFragment : MangaListFragment() {
|
||||
@@ -30,47 +27,14 @@ class FavouritesListFragment : MangaListFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
|
||||
|
||||
if (categoryId != NO_ID) {
|
||||
addMenuProvider(FavouritesListMenuProvider(viewModel))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
if (categoryId != NO_ID) {
|
||||
inflater.inflate(R.menu.opt_favourites_list, menu)
|
||||
menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
|
||||
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
|
||||
val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes)
|
||||
menuItem.isCheckable = true
|
||||
}
|
||||
submenu.setGroupCheckable(R.id.group_order, true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
|
||||
val selectedOrder = viewModel.sortOrder.value
|
||||
for (item in submenu) {
|
||||
val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order)
|
||||
item.isChecked = order == selectedOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when {
|
||||
item.itemId == R.id.action_order -> false
|
||||
item.groupId == R.id.group_order -> {
|
||||
val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false
|
||||
viewModel.setSortOrder(order)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_favourites, menu)
|
||||
return super.onCreateActionMode(mode, menu)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.list
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.iterator
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.titleRes
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
|
||||
|
||||
class FavouritesListMenuProvider(
|
||||
private val viewModel: FavouritesListViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_favourites_list, menu)
|
||||
menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
|
||||
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
|
||||
val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes)
|
||||
menuItem.isCheckable = true
|
||||
}
|
||||
submenu.setGroupCheckable(R.id.group_order, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_order)?.subMenu?.let { submenu ->
|
||||
val selectedOrder = viewModel.sortOrder.value
|
||||
for (item in submenu) {
|
||||
val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order)
|
||||
item.isChecked = order == selectedOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when {
|
||||
menuItem.itemId == R.id.action_order -> false
|
||||
menuItem.groupId == R.id.group_order -> {
|
||||
val order = CategoriesActivity.SORT_ORDERS.getOrNull(menuItem.order) ?: return false
|
||||
viewModel.setSortOrder(order)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class HistoryRepository(
|
||||
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
|
||||
)
|
||||
)
|
||||
trackingRepository.upsert(manga)
|
||||
trackingRepository.syncWithHistory(manga, chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ package org.koitharu.kotatsu.history.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -14,6 +12,7 @@ import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
|
||||
class HistoryListFragment : MangaListFragment() {
|
||||
|
||||
@@ -22,6 +21,7 @@ class HistoryListFragment : MangaListFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addMenuProvider(HistoryListMenuProvider(view.context, viewModel))
|
||||
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
@@ -30,37 +30,6 @@ class HistoryListFragment : MangaListFragment() {
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_history, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_history_grouping)?.isChecked =
|
||||
viewModel.isGroupingEnabled.value == true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
MaterialAlertDialogBuilder(context ?: return false)
|
||||
.setTitle(R.string.clear_history)
|
||||
.setMessage(R.string.text_clear_history_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearHistory()
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
R.id.action_history_grouping -> {
|
||||
viewModel.setGrouping(!item.isChecked)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_history, menu)
|
||||
return super.onCreateActionMode(mode, menu)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.koitharu.kotatsu.history.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class HistoryListMenuProvider(
|
||||
private val context: Context,
|
||||
private val viewModel: HistoryListViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_history, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.clear_history)
|
||||
.setMessage(R.string.text_clear_history_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearHistory()
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
R.id.action_history_grouping -> {
|
||||
viewModel.setGrouping(!menuItem.isChecked)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.collection.ArraySet
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -67,11 +68,6 @@ abstract class MangaListFragment :
|
||||
protected val selectedItems: Set<Manga>
|
||||
get() = collectSelectedItems()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
@@ -98,6 +94,7 @@ abstract class MangaListFragment :
|
||||
setOnRefreshListener(this@MangaListFragment)
|
||||
isEnabled = isSwipeRefreshEnabled
|
||||
}
|
||||
addMenuProvider(MangaListMenuProvider(childFragmentManager))
|
||||
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||
@@ -114,19 +111,6 @@ abstract class MangaListFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_list, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_list_mode -> {
|
||||
ListModeSelectDialog.show(childFragmentManager)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Manga, view: View) {
|
||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
||||
selectionDecoration?.toggleItemChecked(item.id)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class MangaListMenuProvider(
|
||||
private val fragmentManager: FragmentManager,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_list, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_list_mode -> {
|
||||
ListModeSelectDialog.show(fragmentManager)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import androidx.annotation.WorkerThread
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -22,12 +28,6 @@ import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.readText
|
||||
import org.koitharu.kotatsu.utils.ext.resolveName
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
private const val MAX_PARALLELISM = 4
|
||||
|
||||
@@ -37,28 +37,25 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
|
||||
private val filenameFilter = CbzFilter()
|
||||
private val locks = CompositeMutex<Long>()
|
||||
|
||||
override suspend fun getList(
|
||||
offset: Int,
|
||||
query: String?,
|
||||
tags: Set<MangaTag>?,
|
||||
sortOrder: SortOrder?
|
||||
): List<Manga> {
|
||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val files = getAllFiles()
|
||||
val list = coroutineScope {
|
||||
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
|
||||
files.map { file ->
|
||||
getFromFileAsync(file, dispatcher)
|
||||
}.awaitAll()
|
||||
}.filterNotNullTo(ArrayList(files.size))
|
||||
if (!query.isNullOrEmpty()) {
|
||||
val list = getRawList()
|
||||
if (query.isNotEmpty()) {
|
||||
list.retainAll { x ->
|
||||
x.title.contains(query, ignoreCase = true) ||
|
||||
x.altTitle?.contains(query, ignoreCase = true) == true
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val list = getRawList()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
list.retainAll { x ->
|
||||
x.tags.containsAll(tags)
|
||||
@@ -244,7 +241,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
|
||||
}
|
||||
}
|
||||
|
||||
override val sortOrders = emptySet<SortOrder>()
|
||||
override val sortOrders = setOf(SortOrder.ALPHABETICAL)
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||
|
||||
@@ -295,6 +292,16 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
|
||||
locks.unlock(id)
|
||||
}
|
||||
|
||||
private suspend fun getRawList(): ArrayList<Manga> {
|
||||
val files = getAllFiles()
|
||||
return coroutineScope {
|
||||
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
|
||||
files.map { file ->
|
||||
getFromFileAsync(file, dispatcher)
|
||||
}.awaitAll()
|
||||
}.filterNotNullTo(ArrayList(files.size))
|
||||
}
|
||||
|
||||
private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir ->
|
||||
dir.listFiles(filenameFilter)?.toList().orEmpty()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
@@ -19,6 +18,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
|
||||
@@ -48,6 +48,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
|
||||
viewModel.onMangaRemoved.observe(viewLifecycleOwner) { onItemRemoved() }
|
||||
viewModel.importProgress.observe(viewLifecycleOwner, ::onImportProgressChanged)
|
||||
}
|
||||
@@ -77,21 +78,6 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<List<@JvmS
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_local, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_import -> {
|
||||
onEmptyActionClick()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) {
|
||||
if (result.isEmpty()) return
|
||||
viewModel.importFiles(result)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.local.ui
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class LocalListMenuProvider(
|
||||
private val onImportClick: Function0<Unit>,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_local, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_import -> {
|
||||
onImportClick()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.local.ui
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -24,6 +23,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.IOException
|
||||
|
||||
class LocalListViewModel(
|
||||
private val repository: LocalMangaRepository,
|
||||
@@ -115,7 +115,7 @@ class LocalListViewModel(
|
||||
private suspend fun doRefresh() {
|
||||
try {
|
||||
listError.value = null
|
||||
mangaList.value = repository.getList(0)
|
||||
mangaList.value = repository.getList(0, null, null)
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class PageSaveContract : ActivityResultContracts.CreateDocument() {
|
||||
class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
|
||||
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package org.koitharu.kotatsu.remotelist.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -11,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
@@ -22,31 +26,15 @@ class RemoteListFragment : MangaListFragment() {
|
||||
|
||||
private val source by serializableArgument<MangaSource>(ARG_SOURCE)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addMenuProvider(RemoteListMenuProvider())
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_list_remote, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_source_settings -> {
|
||||
startActivity(
|
||||
SettingsActivity.newSourceSettingsIntent(context ?: return false, source)
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
onFilterClick()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||
return super.onCreateActionMode(mode, menu)
|
||||
@@ -60,6 +48,25 @@ class RemoteListFragment : MangaListFragment() {
|
||||
viewModel.resetFilter()
|
||||
}
|
||||
|
||||
private inner class RemoteListMenuProvider: MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_list_remote, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_source_settings -> {
|
||||
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), source))
|
||||
true
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
onFilterClick()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_SOURCE = "provider"
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
|
||||
@@ -35,7 +34,6 @@ class MangaSearchRepository(
|
||||
MangaRepository(source).getList(
|
||||
offset = 0,
|
||||
query = query,
|
||||
sortOrder = SortOrder.POPULARITY
|
||||
)
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
@@ -141,4 +139,4 @@ class MangaSearchRepository(
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.CompositeException
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -78,8 +79,7 @@ class MultiSearchViewModel(
|
||||
listData.value = emptyList()
|
||||
loadingData.value = true
|
||||
query.postValue(q)
|
||||
val errors = searchImpl(q)
|
||||
listError.value = errors.firstOrNull()
|
||||
searchImpl(q)
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
} finally {
|
||||
@@ -88,25 +88,44 @@ class MultiSearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun searchImpl(q: String): List<Throwable> {
|
||||
private suspend fun searchImpl(q: String) {
|
||||
val sources = settings.getMangaSources(includeHidden = false)
|
||||
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||
return coroutineScope {
|
||||
val deferredList = coroutineScope {
|
||||
sources.map { source ->
|
||||
async(dispatcher) {
|
||||
runCatching {
|
||||
val list = MangaRepository(source).getList(offset = 0, query = q)
|
||||
// .sortedBy { x -> x.title.levenshteinDistance(q) }
|
||||
.toUi(ListMode.GRID)
|
||||
if (list.isNotEmpty()) {
|
||||
val item = MultiSearchListModel(source, list)
|
||||
listData.update { x -> x + item }
|
||||
MultiSearchListModel(source, list)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.exceptionOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
val errors = ArrayList<Throwable>()
|
||||
for (deferred in deferredList) {
|
||||
deferred.await()
|
||||
.onSuccess { item ->
|
||||
if (item != null) {
|
||||
listData.update { x -> x + item }
|
||||
}
|
||||
}.onFailure {
|
||||
errors.add(it)
|
||||
}
|
||||
}
|
||||
if (listData.value.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
when (errors.size) {
|
||||
0 -> Unit
|
||||
1 -> throw errors[0]
|
||||
else -> throw CompositeException(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launchWhenResumed {
|
||||
val items = trackerRepo.count()
|
||||
val items = trackerRepo.getLogsCount()
|
||||
pref.summary =
|
||||
pref.context.resources.getQuantityString(R.plurals.items, items, items)
|
||||
}
|
||||
@@ -142,4 +142,4 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.text.style.URLSpan
|
||||
import android.view.View
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -23,6 +29,8 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
|
||||
private const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||
|
||||
class TrackerSettingsFragment :
|
||||
BasePreferenceFragment(R.string.check_for_new_chapters),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@@ -50,6 +58,9 @@ class TrackerSettingsFragment :
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
findPreference<Preference>(KEY_IGNORE_DOZE)?.run {
|
||||
isVisible = isDozeIgnoreAvailable(context)
|
||||
}
|
||||
updateCategoriesSummary()
|
||||
updateNotificationsSummary()
|
||||
}
|
||||
@@ -95,6 +106,10 @@ class TrackerSettingsFragment :
|
||||
startActivity(CategoriesActivity.newIntent(preference.context))
|
||||
true
|
||||
}
|
||||
KEY_IGNORE_DOZE -> {
|
||||
startIgnoreDoseActivity(preference.context)
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
@@ -122,4 +137,34 @@ class TrackerSettingsFragment :
|
||||
pref.summary = getString(R.string.enabled_d_of_d, count[0], count[1])
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
private fun startIgnoreDoseActivity(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
val packageName = context.packageName
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
try {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
"package:$packageName".toUri(),
|
||||
)
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDozeIgnoreAvailable(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return false
|
||||
}
|
||||
val packageName = context.packageName
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return !powerManager.isIgnoringBatteryOptimizations(packageName)
|
||||
}
|
||||
}
|
||||
@@ -36,18 +36,6 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
|
||||
openLink(getString(R.string.url_weblate), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_4PDA -> {
|
||||
openLink(getString(R.string.url_forpda), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_DISCORD -> {
|
||||
openLink(getString(R.string.url_discord), preference.title)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_FEEDBACK_GITHUB -> {
|
||||
openLink(getString(R.string.url_github_issues), preference.title)
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,16 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
private val viewModel by viewModel<BackupViewModel>()
|
||||
|
||||
private var backup: File? = null
|
||||
private val saveFileContract =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
|
||||
val file = backup
|
||||
if (uri != null && file != null) {
|
||||
saveBackup(file, uri)
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
private val saveFileContract = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("*/*")
|
||||
) { uri ->
|
||||
val file = backup
|
||||
if (uri != null && file != null) {
|
||||
saveBackup(file, uri)
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.newsources
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
|
||||
@@ -33,7 +34,7 @@ class NewSourcesViewModel(
|
||||
sources.value = initialList.map {
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = null,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = it.name !in hidden,
|
||||
isDraggable = false,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -20,12 +21,11 @@ import org.koitharu.kotatsu.settings.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
|
||||
class SourcesSettingsFragment :
|
||||
BaseFragment<FragmentSettingsSourcesBinding>(),
|
||||
SourceConfigListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
MenuItem.OnActionExpandListener,
|
||||
RecyclerViewOwner {
|
||||
|
||||
private var reorderHelper: ItemTouchHelper? = null
|
||||
@@ -34,11 +34,6 @@ class SourcesSettingsFragment :
|
||||
override val recyclerView: RecyclerView
|
||||
get() = binding.recyclerView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
@@ -62,6 +57,7 @@ class SourcesSettingsFragment :
|
||||
viewModel.items.observe(viewLifecycleOwner) {
|
||||
sourcesAdapter.items = it
|
||||
}
|
||||
addMenuProvider(SourcesMenuProvider())
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -69,17 +65,6 @@ class SourcesSettingsFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_sources, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
@@ -106,21 +91,39 @@ class SourcesSettingsFragment :
|
||||
viewModel.expandOrCollapse(header.localeId)
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
private inner class SourcesMenuProvider :
|
||||
MenuProvider,
|
||||
MenuItem.OnActionExpandListener,
|
||||
SearchView.OnQueryTextListener {
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.performSearch(newText)
|
||||
return true
|
||||
}
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_sources, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||
return true
|
||||
}
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
(item.actionView as SearchView).setQuery("", false)
|
||||
return true
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
(item.actionView as SearchView).setQuery("", false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.performSearch(newText)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
@@ -82,7 +83,7 @@ class SourcesSettingsViewModel(
|
||||
}
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = null,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = it.name !in hiddenSources,
|
||||
isDraggable = false,
|
||||
)
|
||||
@@ -105,7 +106,7 @@ class SourcesSettingsViewModel(
|
||||
enabledSources.mapTo(result) {
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = getLocaleTitle(it.locale),
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = true,
|
||||
isDraggable = true,
|
||||
)
|
||||
@@ -162,4 +163,4 @@ class SourcesSettingsViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,17 @@ import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
|
||||
fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
|
||||
) {
|
||||
fun sourceConfigHeaderDelegate() =
|
||||
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
|
||||
) {
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.setText(item.titleResId)
|
||||
bind {
|
||||
binding.textViewTitle.setText(item.titleResId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sourceConfigGroupDelegate(
|
||||
listener: SourceConfigListener,
|
||||
@@ -61,6 +63,7 @@ fun sourceConfigItemDelegate(
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.title
|
||||
binding.switchToggle.isChecked = item.isEnabled
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
imageRequest = ImageRequest.Builder(context)
|
||||
.data(item.faviconUrl)
|
||||
.error(R.drawable.ic_favicon_fallback)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.PreferenceAboutLinksBinding
|
||||
|
||||
class AboutLinksPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : Preference(context, attrs), View.OnClickListener {
|
||||
|
||||
init {
|
||||
layoutResource = R.layout.preference_about_links
|
||||
isSelectable = false
|
||||
isPersistent = false
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
val binding = PreferenceAboutLinksBinding.bind(holder.itemView)
|
||||
arrayOf(
|
||||
binding.btn4pda,
|
||||
binding.btnDiscord,
|
||||
binding.btnGithub,
|
||||
binding.btnReddit,
|
||||
binding.btnTwitter,
|
||||
).forEach { button ->
|
||||
TooltipCompat.setTooltipText(button, button.contentDescription)
|
||||
button.setOnClickListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val urlResId = when (v.id) {
|
||||
R.id.btn_4pda -> R.string.url_forpda
|
||||
R.id.btn_discord -> R.string.url_discord
|
||||
R.id.btn_twitter -> R.string.url_twitter
|
||||
R.id.btn_reddit -> R.string.url_reddit
|
||||
R.id.btn_github -> R.string.url_github
|
||||
else -> return
|
||||
}
|
||||
openLink(v.context.getString(urlResId), v.contentDescription)
|
||||
}
|
||||
|
||||
private fun openLink(url: String, title: CharSequence?) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
try {
|
||||
context.startActivity(
|
||||
if (title != null) {
|
||||
Intent.createChooser(intent, title)
|
||||
} else {
|
||||
intent
|
||||
}
|
||||
)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,30 +4,40 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
|
||||
class SuggestionsFragment : MangaListFragment() {
|
||||
|
||||
override val viewModel by viewModel<SuggestionsViewModel>()
|
||||
override val isSwipeRefreshEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addMenuProvider(SuggestionMenuProvider())
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_suggestions, menu)
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||
return super.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
private inner class SuggestionMenuProvider : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_suggestions, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_update -> {
|
||||
SuggestionsWorker.startNow(requireContext())
|
||||
Snackbar.make(
|
||||
@@ -41,17 +51,10 @@ class SuggestionsFragment : MangaListFragment() {
|
||||
startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext()))
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_remote, menu)
|
||||
return super.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = SuggestionsFragment()
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.ui.FeedViewModel
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
@@ -13,5 +14,7 @@ val trackerModule
|
||||
factory { TrackingRepository(get()) }
|
||||
factory { TrackerNotificationChannels(androidContext(), get()) }
|
||||
|
||||
factory { Tracker(get(), get(), get()) }
|
||||
|
||||
viewModel { FeedViewModel(get()) }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||
|
||||
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
|
||||
id = trackLog.id,
|
||||
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
||||
manga = manga.toManga(tags.toMangaTags()),
|
||||
createdAt = Date(trackLog.createdAt)
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "tracks",
|
||||
@@ -19,9 +20,11 @@ import androidx.room.PrimaryKey
|
||||
class TrackEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
|
||||
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
||||
@ColumnInfo(name = "last_chapter_id") val lastChapterId: Long,
|
||||
@ColumnInfo(name = "chapters_new") val newChapters: Int,
|
||||
@ColumnInfo(name = "last_check") val lastCheck: Long,
|
||||
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
|
||||
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "track_logs",
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
class TrackLogWithManga(
|
||||
@Embedded val trackLog: TrackLogEntity,
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
package org.koitharu.kotatsu.tracker.data
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class TracksDao {
|
||||
@@ -19,6 +18,9 @@ abstract class TracksDao {
|
||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract suspend fun findNewChapters(mangaId: Long): Int?
|
||||
|
||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
||||
abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
|
||||
|
||||
@Query("DELETE FROM tracks")
|
||||
abstract suspend fun clear()
|
||||
|
||||
@@ -32,7 +34,7 @@ abstract class TracksDao {
|
||||
abstract suspend fun delete(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
|
||||
abstract suspend fun cleanup()
|
||||
abstract suspend fun gc()
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(entity: TrackEntity) {
|
||||
115
app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt
Normal file
115
app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package org.koitharu.kotatsu.tracker.domain
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
import org.koitharu.kotatsu.tracker.work.TrackingItem
|
||||
|
||||
class Tracker(
|
||||
private val settings: AppSettings,
|
||||
private val repository: TrackingRepository,
|
||||
private val channels: TrackerNotificationChannels,
|
||||
) {
|
||||
|
||||
suspend fun getAllTracks(): List<TrackingItem> {
|
||||
val sources = settings.trackSources
|
||||
if (sources.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val knownIds = HashSet<Manga>()
|
||||
val result = ArrayList<TrackingItem>()
|
||||
// Favourites
|
||||
if (AppSettings.TRACK_FAVOURITES in sources) {
|
||||
val favourites = repository.getAllFavouritesManga()
|
||||
channels.updateChannels(favourites.keys)
|
||||
for ((category, mangaList) in favourites) {
|
||||
if (!category.isTrackingEnabled || mangaList.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
val categoryTracks = repository.getTracks(mangaList)
|
||||
val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
|
||||
channels.getFavouritesChannelId(category.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (track in categoryTracks) {
|
||||
if (knownIds.add(track.manga)) {
|
||||
result.add(TrackingItem(track, channelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// History
|
||||
if (AppSettings.TRACK_HISTORY in sources) {
|
||||
val history = repository.getAllHistoryManga()
|
||||
val historyTracks = repository.getTracks(history)
|
||||
val channelId = if (channels.isHistoryNotificationsEnabled()) {
|
||||
channels.getHistoryChannelId()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (track in historyTracks) {
|
||||
if (knownIds.add(track.manga)) {
|
||||
result.add(TrackingItem(track, channelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
result.trimToSize()
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun gc() {
|
||||
repository.gc()
|
||||
}
|
||||
|
||||
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
|
||||
val manga = MangaRepository(track.manga.source).getDetails(track.manga)
|
||||
val updates = compare(track, manga)
|
||||
if (commit) {
|
||||
repository.saveUpdates(updates)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates {
|
||||
val track = repository.getTrack(manga)
|
||||
val updates = compare(track, manga)
|
||||
if (commit) {
|
||||
repository.saveUpdates(updates)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun deleteTrack(mangaId: Long) {
|
||||
repository.deleteTrack(mangaId)
|
||||
}
|
||||
|
||||
/**
|
||||
* The main functionality of tracker: check new chapters in [manga] comparing to the [track]
|
||||
*/
|
||||
private fun compare(track: MangaTracking, manga: Manga): MangaUpdates {
|
||||
if (track.isEmpty()) {
|
||||
// first check or manga was empty on last check
|
||||
return MangaUpdates(manga, emptyList(), isValid = false)
|
||||
}
|
||||
val chapters = requireNotNull(manga.chapters)
|
||||
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
||||
return when {
|
||||
newChapters.isEmpty() -> {
|
||||
return MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
|
||||
}
|
||||
newChapters.size == chapters.size -> {
|
||||
return MangaUpdates(manga, emptyList(), isValid = false)
|
||||
}
|
||||
else -> {
|
||||
return MangaUpdates(manga, newChapters, isValid = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
package org.koitharu.kotatsu.tracker.domain
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.room.withTransaction
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.MangaTracking
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||
|
||||
private const val NO_ID = 0L
|
||||
|
||||
class TrackingRepository(
|
||||
private val db: MangaDatabase,
|
||||
@@ -21,15 +30,96 @@ class TrackingRepository(
|
||||
return db.tracksDao.findNewChapters(mangaId) ?: 0
|
||||
}
|
||||
|
||||
suspend fun getHistoryManga(): List<Manga> {
|
||||
return db.historyDao.findAllManga().toMangaList()
|
||||
fun observeNewChaptersCount(mangaId: Long): Flow<Int> {
|
||||
return db.tracksDao.observeNewChapters(mangaId).map { it ?: 0 }
|
||||
}
|
||||
|
||||
suspend fun getFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
|
||||
val categories = db.favouriteCategoriesDao.findAll()
|
||||
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
|
||||
categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList()
|
||||
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
||||
val ids = mangaList.mapToSet { it.id }
|
||||
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
|
||||
val idSet = HashSet<Long>()
|
||||
val result = ArrayList<MangaTracking>(mangaList.size)
|
||||
for (item in mangaList) {
|
||||
if (item.source == MangaSource.LOCAL || !idSet.add(item.id)) {
|
||||
continue
|
||||
}
|
||||
val track = tracks[item.id]?.lastOrNull()
|
||||
result += MangaTracking(
|
||||
manga = item,
|
||||
lastChapterId = track?.lastChapterId ?: NO_ID,
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun getTrack(manga: Manga): MangaTracking {
|
||||
val track = db.tracksDao.find(manga.id)
|
||||
return MangaTracking(
|
||||
manga = manga,
|
||||
lastChapterId = track?.lastChapterId ?: NO_ID,
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun deleteTrack(mangaId: Long) {
|
||||
db.tracksDao.delete(mangaId)
|
||||
}
|
||||
|
||||
suspend fun getTrackingLog(offset: Int, limit: Int): List<TrackingLogItem> {
|
||||
return db.trackLogsDao.findAll(offset, limit).map { x ->
|
||||
x.toTrackingLogItem()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLogsCount() = db.trackLogsDao.count()
|
||||
|
||||
suspend fun clearLogs() = db.trackLogsDao.clear()
|
||||
|
||||
suspend fun gc() {
|
||||
db.withTransaction {
|
||||
db.tracksDao.gc()
|
||||
db.trackLogsDao.gc()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveUpdates(updates: MangaUpdates) {
|
||||
db.withTransaction {
|
||||
val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
|
||||
db.tracksDao.upsert(track)
|
||||
if (updates.isValid && updates.newChapters.isNotEmpty()) {
|
||||
val logEntity = TrackLogEntity(
|
||||
mangaId = updates.manga.id,
|
||||
chapters = updates.newChapters.joinToString("\n") { x -> x.name },
|
||||
createdAt = System.currentTimeMillis(),
|
||||
)
|
||||
db.trackLogsDao.insert(logEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncWithHistory(manga: Manga, chapterId: Long) {
|
||||
val chapters = manga.chapters ?: return
|
||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
||||
val track = getOrCreateTrack(manga.id)
|
||||
val lastNewChapterIndex = chapters.size - track.newChapters
|
||||
val lastChapterId = chapters.lastOrNull()?.id ?: NO_ID
|
||||
val entity = TrackEntity(
|
||||
mangaId = manga.id,
|
||||
totalChapters = chapters.size,
|
||||
lastChapterId = lastChapterId,
|
||||
newChapters = when {
|
||||
track.newChapters == 0 -> 0
|
||||
chapterIndex < 0 -> track.newChapters
|
||||
chapterIndex > lastNewChapterIndex -> chapters.lastIndex - chapterIndex
|
||||
else -> track.newChapters
|
||||
},
|
||||
lastCheck = System.currentTimeMillis(),
|
||||
lastNotifiedChapterId = lastChapterId,
|
||||
)
|
||||
db.tracksDao.upsert(entity)
|
||||
}
|
||||
|
||||
suspend fun getCategoriesCount(): IntArray {
|
||||
@@ -40,81 +130,39 @@ class TrackingRepository(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
||||
val ids = mangaList.mapToSet { it.id }
|
||||
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
|
||||
return mangaList // TODO optimize
|
||||
.filterNot { it.source == MangaSource.LOCAL }
|
||||
.distinctBy { it.id }
|
||||
.map { manga ->
|
||||
val track = tracks[manga.id]?.singleOrNull()
|
||||
MangaTracking(
|
||||
manga = manga,
|
||||
knownChaptersCount = track?.totalChapters ?: -1,
|
||||
lastChapterId = track?.lastChapterId ?: 0L,
|
||||
lastNotifiedChapterId = track?.lastNotifiedChapterId ?: 0L,
|
||||
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTrackingLog(offset: Int, limit: Int): List<TrackingLogItem> {
|
||||
return db.trackLogsDao.findAll(offset, limit).map { x ->
|
||||
x.toTrackingLogItem()
|
||||
suspend fun getAllFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
|
||||
val categories = db.favouriteCategoriesDao.findAll()
|
||||
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
|
||||
categoryEntity.toFavouriteCategory() to
|
||||
db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun count() = db.trackLogsDao.count()
|
||||
|
||||
suspend fun clearLogs() = db.trackLogsDao.clear()
|
||||
|
||||
suspend fun cleanup() {
|
||||
db.withTransaction {
|
||||
db.tracksDao.cleanup()
|
||||
db.trackLogsDao.cleanup()
|
||||
}
|
||||
suspend fun getAllHistoryManga(): List<Manga> {
|
||||
return db.historyDao.findAllManga().toMangaList()
|
||||
}
|
||||
|
||||
suspend fun storeTrackResult(
|
||||
mangaId: Long,
|
||||
knownChaptersCount: Int,
|
||||
lastChapterId: Long,
|
||||
newChapters: List<MangaChapter>,
|
||||
previousTrackChapterId: Long
|
||||
) {
|
||||
db.withTransaction {
|
||||
val entity = TrackEntity(
|
||||
mangaId = mangaId,
|
||||
newChapters = newChapters.size,
|
||||
lastCheck = System.currentTimeMillis(),
|
||||
lastChapterId = lastChapterId,
|
||||
totalChapters = knownChaptersCount,
|
||||
lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId
|
||||
)
|
||||
db.tracksDao.upsert(entity)
|
||||
val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId }
|
||||
if (foundChapters.isNotEmpty()) {
|
||||
val logEntity = TrackLogEntity(
|
||||
mangaId = mangaId,
|
||||
chapters = foundChapters.joinToString("\n") { x -> x.name },
|
||||
createdAt = System.currentTimeMillis()
|
||||
)
|
||||
db.trackLogsDao.insert(logEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsert(manga: Manga) {
|
||||
val chapters = manga.chapters ?: return
|
||||
val entity = TrackEntity(
|
||||
mangaId = manga.id,
|
||||
totalChapters = chapters.size,
|
||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||
private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
|
||||
return db.tracksDao.find(mangaId) ?: TrackEntity(
|
||||
mangaId = mangaId,
|
||||
totalChapters = 0,
|
||||
lastChapterId = 0L,
|
||||
newChapters = 0,
|
||||
lastCheck = System.currentTimeMillis(),
|
||||
lastNotifiedChapterId = 0L
|
||||
lastCheck = 0L,
|
||||
lastNotifiedChapterId = 0L,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity {
|
||||
val chapters = updates.manga.chapters.orEmpty()
|
||||
return TrackEntity(
|
||||
mangaId = mangaId,
|
||||
totalChapters = chapters.size,
|
||||
lastChapterId = chapters.lastOrNull()?.id ?: NO_ID,
|
||||
newChapters = if (updates.isValid) newChapters + updates.newChapters.size else 0,
|
||||
lastCheck = System.currentTimeMillis(),
|
||||
lastNotifiedChapterId = NO_ID,
|
||||
)
|
||||
db.tracksDao.upsert(entity)
|
||||
}
|
||||
|
||||
private fun Collection<MangaEntity>.toMangaList() = map { it.toManga(emptySet()) }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.koitharu.kotatsu.tracker.domain.model
|
||||
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class MangaTracking(
|
||||
val manga: Manga,
|
||||
val lastChapterId: Long,
|
||||
val lastCheck: Date?,
|
||||
) {
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return lastChapterId == 0L
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaTracking
|
||||
|
||||
if (manga != other.manga) return false
|
||||
if (lastChapterId != other.lastChapterId) return false
|
||||
if (lastCheck != other.lastCheck) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + lastChapterId.hashCode()
|
||||
result = 31 * result + (lastCheck?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.tracker.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
|
||||
class MangaUpdates(
|
||||
val manga: Manga,
|
||||
val newChapters: List<MangaChapter>,
|
||||
val isValid: Boolean,
|
||||
) {
|
||||
|
||||
fun isNotEmpty() = newChapters.isNotEmpty()
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
package org.koitharu.kotatsu.tracker.domain.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
data class TrackingLogItem(
|
||||
val id: Long,
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.koitharu.kotatsu.tracker.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -21,9 +22,9 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
|
||||
class FeedFragment :
|
||||
BaseFragment<FragmentFeedBinding>(),
|
||||
@@ -33,15 +34,9 @@ class FeedFragment :
|
||||
private val viewModel by viewModel<FeedViewModel>()
|
||||
|
||||
private var feedAdapter: FeedAdapter? = null
|
||||
private var updateStatusSnackbar: Snackbar? = null
|
||||
private var paddingVertical = 0
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
@@ -63,49 +58,20 @@ class FeedFragment :
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
}
|
||||
binding.swipeRefreshLayout.isEnabled = false
|
||||
addMenuProvider(FeedMenuProvider(binding.recyclerView, viewModel))
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, this::onError)
|
||||
viewModel.onFeedCleared.observe(viewLifecycleOwner) {
|
||||
onFeedCleared()
|
||||
}
|
||||
TrackWorker.getProgressLiveData(view.context.applicationContext)
|
||||
.observe(viewLifecycleOwner, this::onUpdateProgressChanged)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.opt_feed, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_update -> {
|
||||
TrackWorker.startNow(requireContext())
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
R.string.feed_will_update_soon,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
true
|
||||
}
|
||||
R.id.action_clear_feed -> {
|
||||
MaterialAlertDialogBuilder(context ?: return false)
|
||||
.setTitle(R.string.clear_updates_feed)
|
||||
.setMessage(R.string.text_clear_updates_feed_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearFeed()
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
TrackWorker.getIsRunningLiveData(view.context.applicationContext)
|
||||
.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
feedAdapter = null
|
||||
updateStatusSnackbar = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -147,23 +113,8 @@ class FeedFragment :
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun onUpdateProgressChanged(progress: Progress?) {
|
||||
if (progress == null) {
|
||||
updateStatusSnackbar?.dismiss()
|
||||
updateStatusSnackbar = null
|
||||
return
|
||||
}
|
||||
val summaryText = getString(
|
||||
R.string.chapters_checking_progress,
|
||||
progress.value + 1,
|
||||
progress.total
|
||||
)
|
||||
updateStatusSnackbar?.setText(summaryText) ?: run {
|
||||
val snackbar =
|
||||
Snackbar.make(binding.recyclerView, summaryText, Snackbar.LENGTH_INDEFINITE)
|
||||
updateStatusSnackbar = snackbar
|
||||
snackbar.show()
|
||||
}
|
||||
private fun onIsTrackerRunningChanged(isRunning: Boolean) {
|
||||
binding.swipeRefreshLayout.isRefreshing = isRunning
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.koitharu.kotatsu.tracker.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
|
||||
class FeedMenuProvider(
|
||||
private val snackbarHost: View,
|
||||
private val viewModel: FeedViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
private val context: Context
|
||||
get() = snackbarHost.context
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_feed, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_update -> {
|
||||
TrackWorker.startNow(context)
|
||||
Snackbar.make(
|
||||
snackbarHost,
|
||||
R.string.feed_will_update_soon,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
true
|
||||
}
|
||||
R.id.action_clear_feed -> {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.clear_updates_feed)
|
||||
.setMessage(R.string.text_clear_updates_feed_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearFeed()
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.tracker.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
@@ -9,16 +11,14 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(
|
||||
private val repository: TrackingRepository
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||
|
||||
fun TrackingLogItem.toFeedItem() = FeedItem(
|
||||
id = id,
|
||||
|
||||
@@ -15,179 +15,86 @@ import androidx.work.*
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||
import org.koitharu.kotatsu.utils.PendingIntentCompat
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.trySetForeground
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
|
||||
class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams), KoinComponent {
|
||||
CoroutineWorker(context, workerParams),
|
||||
KoinComponent {
|
||||
|
||||
private val notificationManager by lazy {
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
private val coil by inject<ImageLoader>()
|
||||
private val repository by inject<TrackingRepository>()
|
||||
private val settings by inject<AppSettings>()
|
||||
private val channels by inject<TrackerNotificationChannels>()
|
||||
private val tracker by inject<Tracker>()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (!settings.isTrackerEnabled) {
|
||||
return Result.success()
|
||||
return Result.success(workDataOf(0, 0))
|
||||
}
|
||||
if (TAG in tags) { // not expedited
|
||||
trySetForeground()
|
||||
}
|
||||
val tracks = getAllTracks()
|
||||
val tracks = tracker.getAllTracks()
|
||||
if (tracks.isEmpty()) {
|
||||
return Result.success(workDataOf(0, 0))
|
||||
}
|
||||
|
||||
val updates = checkUpdatesAsync(tracks)
|
||||
val results = updates.awaitAll()
|
||||
tracker.gc()
|
||||
|
||||
var success = 0
|
||||
val workData = Data.Builder()
|
||||
.putInt(DATA_TOTAL, tracks.size)
|
||||
for ((index, item) in tracks.withIndex()) {
|
||||
val (track, channelId) = item
|
||||
val details = runCatching {
|
||||
MangaRepository(track.manga.source).getDetails(track.manga)
|
||||
}.getOrNull()
|
||||
workData.putInt(DATA_PROGRESS, index)
|
||||
setProgress(workData.build())
|
||||
val chapters = details?.chapters ?: continue
|
||||
when {
|
||||
track.knownChaptersCount == -1 -> { // first check
|
||||
repository.storeTrackResult(
|
||||
mangaId = track.manga.id,
|
||||
knownChaptersCount = chapters.size,
|
||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||
previousTrackChapterId = 0L,
|
||||
newChapters = emptyList()
|
||||
)
|
||||
}
|
||||
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check
|
||||
repository.storeTrackResult(
|
||||
mangaId = track.manga.id,
|
||||
knownChaptersCount = 0,
|
||||
lastChapterId = 0L,
|
||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
||||
newChapters = chapters
|
||||
)
|
||||
showNotification(details, channelId, chapters)
|
||||
}
|
||||
chapters.size == track.knownChaptersCount -> {
|
||||
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
||||
// manga was not updated. skip
|
||||
} else {
|
||||
// number of chapters still the same, bu last chapter changed.
|
||||
// maybe some chapters are removed. we need to find last known chapter
|
||||
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
|
||||
if (knownChapter == -1) {
|
||||
// confuse. reset anything
|
||||
repository.storeTrackResult(
|
||||
mangaId = track.manga.id,
|
||||
knownChaptersCount = chapters.size,
|
||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
||||
newChapters = emptyList()
|
||||
)
|
||||
} else {
|
||||
val newChapters = chapters.takeLast(chapters.size - knownChapter + 1)
|
||||
repository.storeTrackResult(
|
||||
mangaId = track.manga.id,
|
||||
knownChaptersCount = knownChapter + 1,
|
||||
lastChapterId = track.lastChapterId,
|
||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
||||
newChapters = newChapters
|
||||
)
|
||||
showNotification(
|
||||
details,
|
||||
channelId,
|
||||
newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val newChapters = chapters.takeLast(chapters.size - track.knownChaptersCount)
|
||||
repository.storeTrackResult(
|
||||
mangaId = track.manga.id,
|
||||
knownChaptersCount = track.knownChaptersCount,
|
||||
lastChapterId = track.lastChapterId,
|
||||
previousTrackChapterId = track.lastNotifiedChapterId,
|
||||
newChapters = newChapters,
|
||||
)
|
||||
showNotification(
|
||||
manga = track.manga,
|
||||
channelId = channelId,
|
||||
newChapters = newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId },
|
||||
)
|
||||
}
|
||||
var failed = 0
|
||||
results.forEach { x ->
|
||||
if (x == null) {
|
||||
failed++
|
||||
} else {
|
||||
success++
|
||||
}
|
||||
success++
|
||||
}
|
||||
repository.cleanup()
|
||||
return if (success == 0) {
|
||||
Result.retry()
|
||||
val resultData = workDataOf(success, failed)
|
||||
return if (success == 0 && failed != 0) {
|
||||
Result.failure(resultData)
|
||||
} else {
|
||||
Result.success()
|
||||
Result.success(resultData)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAllTracks(): List<TrackingItem> {
|
||||
val sources = settings.trackSources
|
||||
if (sources.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val knownIds = HashSet<Manga>()
|
||||
val result = ArrayList<TrackingItem>()
|
||||
// Favourites
|
||||
if (AppSettings.TRACK_FAVOURITES in sources) {
|
||||
val favourites = repository.getFavouritesManga()
|
||||
channels.updateChannels(favourites.keys)
|
||||
for ((category, mangaList) in favourites) {
|
||||
if (!category.isTrackingEnabled || mangaList.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
val categoryTracks = repository.getTracks(mangaList)
|
||||
val channelId = if (channels.isFavouriteNotificationsEnabled(category)) {
|
||||
channels.getFavouritesChannelId(category.id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (track in categoryTracks) {
|
||||
if (knownIds.add(track.manga)) {
|
||||
result.add(TrackingItem(track, channelId))
|
||||
}
|
||||
private suspend fun checkUpdatesAsync(tracks: List<TrackingItem>): List<Deferred<MangaUpdates?>> {
|
||||
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||
val deferredList = coroutineScope {
|
||||
tracks.map { (track, channelId) ->
|
||||
async(dispatcher) {
|
||||
runCatching {
|
||||
tracker.fetchUpdates(track, commit = true)
|
||||
}.onSuccess { updates ->
|
||||
if (updates.isValid && updates.isNotEmpty()) {
|
||||
showNotification(
|
||||
manga = updates.manga,
|
||||
channelId = channelId,
|
||||
newChapters = updates.newChapters,
|
||||
)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
// History
|
||||
if (AppSettings.TRACK_HISTORY in sources) {
|
||||
val history = repository.getHistoryManga()
|
||||
val historyTracks = repository.getTracks(history)
|
||||
val channelId = if (channels.isHistoryNotificationsEnabled()) {
|
||||
channels.getHistoryChannelId()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (track in historyTracks) {
|
||||
if (knownIds.add(track.manga)) {
|
||||
result.add(TrackingItem(track, channelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
result.trimToSize()
|
||||
return result
|
||||
return deferredList
|
||||
}
|
||||
|
||||
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
|
||||
@@ -198,8 +105,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary)
|
||||
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||
val summary = applicationContext.resources.getQuantityString(
|
||||
R.plurals.new_chapters,
|
||||
newChapters.size, newChapters.size
|
||||
R.plurals.new_chapters, newChapters.size, newChapters.size
|
||||
)
|
||||
with(builder) {
|
||||
setContentText(summary)
|
||||
@@ -207,10 +113,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
setNumber(newChapters.size)
|
||||
setLargeIcon(
|
||||
coil.execute(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(manga.coverUrl)
|
||||
.referer(manga.publicUrl)
|
||||
.build()
|
||||
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build()
|
||||
).toBitmapOrNull()
|
||||
)
|
||||
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||
@@ -224,8 +127,10 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
val intent = DetailsActivity.newIntent(applicationContext, manga)
|
||||
setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
applicationContext, id,
|
||||
intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
applicationContext,
|
||||
id,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
setAutoCancel(true)
|
||||
@@ -255,9 +160,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
val title = applicationContext.getString(R.string.check_for_new_chapters)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
WORKER_CHANNEL_ID,
|
||||
title,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
WORKER_CHANNEL_ID, title, NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
channel.setShowBadge(false)
|
||||
channel.enableVibration(false)
|
||||
@@ -266,69 +169,52 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setDefaults(0)
|
||||
.setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark))
|
||||
.setSilent(true)
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID).setContentTitle(title)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN).setDefaults(0)
|
||||
.setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)).setSilent(true)
|
||||
.setProgress(0, 0, true).setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED).setOngoing(true).build()
|
||||
|
||||
return ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun workDataOf(success: Int, failed: Int): Data {
|
||||
return Data.Builder()
|
||||
.putInt(DATA_KEY_SUCCESS, success)
|
||||
.putInt(DATA_KEY_FAILED, failed)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val WORKER_CHANNEL_ID = "track_worker"
|
||||
private const val WORKER_NOTIFICATION_ID = 35
|
||||
private const val DATA_PROGRESS = "progress"
|
||||
private const val DATA_TOTAL = "total"
|
||||
private const val TAG = "tracking"
|
||||
private const val TAG_ONESHOT = "tracking_oneshot"
|
||||
private const val MAX_PARALLELISM = 4
|
||||
private const val DATA_KEY_SUCCESS = "success"
|
||||
private const val DATA_KEY_FAILED = "failed"
|
||||
|
||||
fun setup(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val request =
|
||||
PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS).setConstraints(constraints).addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES).build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
fun startNow(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG_ONESHOT)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val request = OneTimeWorkRequestBuilder<TrackWorker>().setConstraints(constraints).addTag(TAG_ONESHOT)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).build()
|
||||
WorkManager.getInstance(context).enqueue(request)
|
||||
}
|
||||
|
||||
fun getProgressLiveData(context: Context): LiveData<Progress?> {
|
||||
return WorkManager.getInstance(context)
|
||||
.getWorkInfosByTagLiveData(TAG)
|
||||
.map { list ->
|
||||
list.find { work ->
|
||||
work.state == WorkInfo.State.RUNNING
|
||||
}?.let { workInfo ->
|
||||
Progress(
|
||||
value = workInfo.progress.getInt(DATA_PROGRESS, 0),
|
||||
total = workInfo.progress.getInt(DATA_TOTAL, -1)
|
||||
).takeUnless { it.isIndeterminate }
|
||||
}
|
||||
}
|
||||
fun getIsRunningLiveData(context: Context): LiveData<Boolean> {
|
||||
val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build()
|
||||
return WorkManager.getInstance(context).getWorkInfosLiveData(query).map { works ->
|
||||
works.any { x -> x.state == WorkInfo.State.RUNNING }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaTracking
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
|
||||
class TrackingItem(
|
||||
val tracking: MangaTracking,
|
||||
|
||||
@@ -2,9 +2,11 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import java.io.Serializable
|
||||
|
||||
@@ -43,4 +45,8 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
|
||||
if (!manager.isStateSaved) {
|
||||
show(manager, tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.addMenuProvider(provider: MenuProvider) {
|
||||
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
@@ -3,12 +3,14 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.*
|
||||
|
||||
fun LocaleListCompat.toList(): List<Locale> = createList(size()) { i -> get(i) }
|
||||
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw kotlin.NoSuchElementException()
|
||||
|
||||
fun LocaleListCompat.toList(): List<Locale> = createList(size()) { i -> getOrThrow(i) }
|
||||
|
||||
operator fun LocaleListCompat.iterator() = object : Iterator<Locale> {
|
||||
private var index = 0
|
||||
override fun hasNext(): Boolean = index < size()
|
||||
override fun next(): Locale = get(index++)
|
||||
override fun next(): Locale = getOrThrow(index++)
|
||||
}
|
||||
|
||||
inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
|
||||
@@ -17,7 +19,7 @@ inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
|
||||
): C {
|
||||
val len = size()
|
||||
for (i in 0 until len) {
|
||||
val item = get(i)
|
||||
val item = get(i) ?: continue
|
||||
destination.add(block(item))
|
||||
}
|
||||
return destination
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
11
app/src/main/res/drawable/ic_4pda.xml
Normal file
11
app/src/main/res/drawable/ic_4pda.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M426.1,112 L112,545.6l0,247.7l486.7,0l0,118.8l313.3,0L912,112ZM599.5,312L599.5,577.6L390.1,577.6Z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_discord.xml
Normal file
11
app/src/main/res/drawable/ic_discord.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20.349,4.389a0.06,0.06 0,0 0,-0.032 -0.029,19.77 19.77,0 0,0 -4.885,-1.515 0.075,0.075 0,0 0,-0.079 0.037c-0.209,0.375 -0.444,0.865 -0.607,1.249a18.35,18.35 0,0 0,-5.487 0,12.583 12.583,0 0,0 -0.617,-1.249 0.078,0.078 0,0 0,-0.079 -0.037,19.746 19.746,0 0,0 -4.886,1.515 0.06,0.06 0,0 0,-0.031 0.028c-3.111,4.648 -3.964,9.183 -3.545,13.66a0.082,0.082 0,0 0,0.031 0.057,19.912 19.912,0 0,0 5.993,3.03 0.075,0.075 0,0 0,0.084 -0.028c0.461,-0.631 0.873,-1.296 1.226,-1.994a0.076,0.076 0,0 0,-0.042 -0.106,13.172 13.172,0 0,1 -1.872,-0.892 0.077,0.077 0,0 1,-0.007 -0.128c0.125,-0.094 0.251,-0.192 0.371,-0.292a0.078,0.078 0,0 1,0.078 -0.011c3.928,1.794 8.179,1.794 12.062,0a0.073,0.073 0,0 1,0.077 0.01c0.122,0.099 0.247,0.199 0.373,0.293a0.078,0.078 0,0 1,-0.007 0.128c-0.597,0.349 -1.22,0.645 -1.873,0.892a0.075,0.075 0,0 0,-0.04 0.106c0.359,0.697 0.771,1.362 1.225,1.993a0.077,0.077 0,0 0,0.085 0.029,19.842 19.842,0 0,0 6.003,-3.03 0.077,0.077 0,0 0,0.03 -0.056c0.499,-5.177 -0.839,-9.674 -3.549,-13.66zM8.02,15.322c-1.183,0 -2.157,-1.087 -2.157,-2.419 0,-1.334 0.956,-2.42 2.157,-2.42 1.21,0 2.176,1.095 2.157,2.42 0,1.332 -0.955,2.419 -2.157,2.419zM15.995,15.322c-1.184,0 -2.157,-1.087 -2.157,-2.419 0,-1.334 0.955,-2.42 2.157,-2.42 1.211,0 2.175,1.095 2.156,2.42 0,1.332 -0.945,2.419 -2.156,2.419z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_github.xml
Normal file
11
app/src/main/res/drawable/ic_github.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_reddit.xml
Normal file
11
app/src/main/res/drawable/ic_reddit.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.5 15.41C14.58 15.5 14.58 15.69 14.5 15.8C13.77 16.5 12.41 16.56 12 16.56C11.61 16.56 10.25 16.5 9.54 15.8C9.44 15.69 9.44 15.5 9.54 15.41C9.65 15.31 9.82 15.31 9.92 15.41C10.38 15.87 11.33 16 12 16C12.69 16 13.66 15.87 14.1 15.41C14.21 15.31 14.38 15.31 14.5 15.41M10.75 13.04C10.75 12.47 10.28 12 9.71 12C9.14 12 8.67 12.47 8.67 13.04C8.67 13.61 9.14 14.09 9.71 14.08C10.28 14.08 10.75 13.61 10.75 13.04M14.29 12C13.72 12 13.25 12.5 13.25 13.05S13.72 14.09 14.29 14.09C14.86 14.09 15.33 13.61 15.33 13.05C15.33 12.5 14.86 12 14.29 12M22 12C22 17.5 17.5 22 12 22S2 17.5 2 12C2 6.5 6.5 2 12 2S22 6.5 22 12M18.67 12C18.67 11.19 18 10.54 17.22 10.54C16.82 10.54 16.46 10.7 16.2 10.95C15.2 10.23 13.83 9.77 12.3 9.71L12.97 6.58L15.14 7.05C15.16 7.6 15.62 8.04 16.18 8.04C16.75 8.04 17.22 7.57 17.22 7C17.22 6.43 16.75 5.96 16.18 5.96C15.77 5.96 15.41 6.2 15.25 6.55L12.82 6.03C12.75 6 12.68 6.03 12.63 6.07C12.57 6.11 12.54 6.17 12.53 6.24L11.79 9.72C10.24 9.77 8.84 10.23 7.82 10.96C7.56 10.71 7.2 10.56 6.81 10.56C6 10.56 5.35 11.21 5.35 12C5.35 12.61 5.71 13.11 6.21 13.34C6.19 13.5 6.18 13.62 6.18 13.78C6.18 16 8.79 17.85 12 17.85C15.23 17.85 17.85 16.03 17.85 13.78C17.85 13.64 17.84 13.5 17.81 13.34C18.31 13.11 18.67 12.6 18.67 12Z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_twitter.xml
Normal file
11
app/src/main/res/drawable/ic_twitter.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.7,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
|
||||
</vector>
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".core.ui.CrashActivity">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"
|
||||
android:textIsSelectable="true" />
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_report"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:drawableEnd="@android:drawable/ic_menu_set_as"
|
||||
android:text="@string/report_github" />
|
||||
|
||||
<LinearLayout
|
||||
style="?buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_close"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/close" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_restart"
|
||||
style="?buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/restart" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,17 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/recyclerView"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/list_spacing"
|
||||
android:paddingRight="@dimen/list_spacing"
|
||||
android:paddingTop="@dimen/grid_spacing_outer"
|
||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_feed" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/list_spacing"
|
||||
android:paddingTop="@dimen/grid_spacing_outer"
|
||||
android:paddingRight="@dimen/list_spacing"
|
||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||
app:fastScrollEnabled="true"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_feed" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@@ -3,8 +3,9 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
@@ -17,16 +18,35 @@
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
tools:text="@tools:sample/lorem[1]" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
tools:text="@tools:sample/lorem[15]" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
tools:text="English" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_toggle"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/navigation_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -16,7 +17,8 @@
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_totoro" />
|
||||
android:src="@drawable/ic_totoro"
|
||||
app:tint="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
|
||||
64
app/src/main/res/layout/preference_about_links.xml
Normal file
64
app/src/main/res/layout/preference_about_links.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_4pda"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="4PDA"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_4pda"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_discord"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Discord"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_discord"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_twitter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Twitter"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_twitter"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_reddit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Reddit"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_reddit"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_github"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="GitHub"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_github"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -294,4 +294,8 @@
|
||||
<string name="default_mode">Standard-Modus</string>
|
||||
<string name="detect_reader_mode">Automatische Erkennung des Lesegerätmodus</string>
|
||||
<string name="detect_reader_mode_summary">Automatisch erkennen, ob ein Manga ein Webtoon ist</string>
|
||||
<string name="disable_battery_optimization">Batterieoptimierung deaktivieren</string>
|
||||
<string name="disable_battery_optimization_summary">Hilft bei der Überprüfung von Hintergrundaktualisierungen</string>
|
||||
<string name="send">Senden</string>
|
||||
<string name="crash_text">Etwas ist schief gelaufen. Bitte senden Sie einen Fehlerbericht an die Entwickler, damit wir das Problem beheben können.</string>
|
||||
</resources>
|
||||
@@ -268,4 +268,30 @@
|
||||
<string name="removal_completed">Remoción completada</string>
|
||||
<string name="batch_manga_save_confirm">¿Estás seguro de que quieres descargar todos los manga seleccionados con todos sus capítulos\? Esta acción puede consumir mucho tráfico y almacenamiento</string>
|
||||
<string name="text_delete_local_manga_batch">¿Eliminar elementos seleccionados del dispositivo de forma permanente\?</string>
|
||||
<string name="hide">Ocultar</string>
|
||||
<string name="download_slowdown">Ralentización de la descarga</string>
|
||||
<string name="new_sources_text">Nuevas fuentes de manga disponibles</string>
|
||||
<string name="parallel_downloads">Descargas en paralelo</string>
|
||||
<string name="download_slowdown_summary">Ayuda a evitar el bloqueo de tu dirección IP</string>
|
||||
<string name="local_manga_processing">Procesamiento de manga guardado</string>
|
||||
<string name="check_new_chapters_title">Comprueba si hay nuevos capítulos y notifica sobre ellos</string>
|
||||
<string name="show_notification_new_chapters_on">Recibirás notificaciones sobre las actualizaciones del manga que estés leyendo</string>
|
||||
<string name="bookmark_add">Añadir marcador</string>
|
||||
<string name="bookmarks">Marcadores</string>
|
||||
<string name="undo">Deshacer</string>
|
||||
<string name="bookmark_removed">Marcador eliminado</string>
|
||||
<string name="removed_from_history">Eliminado del historial</string>
|
||||
<string name="bookmark_remove">Eliminar marcador</string>
|
||||
<string name="bookmark_added">Añadido a favoritos</string>
|
||||
<string name="dns_over_https">DNS mediante HTTPS</string>
|
||||
<string name="default_mode">Modo predeterminado</string>
|
||||
<string name="detect_reader_mode">Autodetectar modo de lectura</string>
|
||||
<string name="detect_reader_mode_summary">Detecta automáticamente si el manga es un webtoon</string>
|
||||
<string name="chapters_will_removed_background">Los capítulos se eliminarán en segundo plano. Puede llevar algún tiempo</string>
|
||||
<string name="show_notification_new_chapters_off">No recibirás notificaciones, pero los nuevos capítulos aparecerán destacados en las listas</string>
|
||||
<string name="notifications_enable">Activar las notificaciones</string>
|
||||
<string name="empty_favourite_categories">No hay categorías favoritas</string>
|
||||
<string name="name">Nombre</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="edit_category">Editar categoría</string>
|
||||
</resources>
|
||||
@@ -294,4 +294,7 @@
|
||||
<string name="detect_reader_mode">Lukijan automaattinen tunnistaminen</string>
|
||||
<string name="detect_reader_mode_summary">Tunnista automaattisesti, onko manga webtoon</string>
|
||||
<string name="default_mode">Oletustila</string>
|
||||
<string name="disable_battery_optimization">Poista akun optimoinnin käytöstä</string>
|
||||
<string name="disable_battery_optimization_summary">Auttaa taustapäivitystarkastuksissa</string>
|
||||
<string name="crash_text">Jokin meni pieleen. Lähetä vikailmoitus kehittäjille, jotta voimme korjata sen.</string>
|
||||
</resources>
|
||||
@@ -294,4 +294,8 @@
|
||||
<string name="default_mode">Mode par défaut</string>
|
||||
<string name="detect_reader_mode">Mode de détection automatique du lecteur</string>
|
||||
<string name="detect_reader_mode_summary">Détecter automatiquement si un manga est un webtoon</string>
|
||||
<string name="disable_battery_optimization">Désactiver l\'optimisation de la batterie</string>
|
||||
<string name="disable_battery_optimization_summary">Aide à la vérification des mises à jour des antécédents</string>
|
||||
<string name="crash_text">Un problème est survenu. Veuillez soumettre un rapport de bogue aux développeurs pour nous aider à le corriger.</string>
|
||||
<string name="send">Envoyer</string>
|
||||
</resources>
|
||||
27
app/src/main/res/values-in/plurals.xml
Normal file
27
app/src/main/res/values-in/plurals.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="chapters_from_x">
|
||||
<item quantity="other">%1$d bab dari %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="pages">
|
||||
<item quantity="other">Total %1$d halaman</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="other">%1$d item</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="other">%1$d bab baru</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="other">%1$d bab</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="other">%1$d menit yang lalu</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="other">%1$d jam yang lalu</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="other">%1$d hari yang lalu</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
291
app/src/main/res/values-in/strings.xml
Normal file
291
app/src/main/res/values-in/strings.xml
Normal file
@@ -0,0 +1,291 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="close_menu">Tutup bilah</string>
|
||||
<string name="open_menu">Buka bilah</string>
|
||||
<string name="local_storage">Penyimpanan lokal</string>
|
||||
<string name="favourites">Favorit</string>
|
||||
<string name="history">Riwayat</string>
|
||||
<string name="error_occurred">Terjadi galat</string>
|
||||
<string name="network_error">Tidak bisa menyambung ke internet</string>
|
||||
<string name="details">Detail</string>
|
||||
<string name="grid">Kisi</string>
|
||||
<string name="list_mode">Mode daftar</string>
|
||||
<string name="settings">Pengaturan</string>
|
||||
<string name="remote_sources">Sumber jarak jauh</string>
|
||||
<string name="loading_">Memuat…</string>
|
||||
<string name="computing_">Menghitung…</string>
|
||||
<string name="chapter_d_of_d">Bab %1$d dari %2$d</string>
|
||||
<string name="close">Tutup</string>
|
||||
<string name="try_again">Coba lagi</string>
|
||||
<string name="nothing_found">Tidak ditemukan apa-apa</string>
|
||||
<string name="history_is_empty">Belum ada riwayat</string>
|
||||
<string name="read">Baca</string>
|
||||
<string name="you_have_not_favourites_yet">Belum ada favorit</string>
|
||||
<string name="add_to_favourites">Favoritkan ini</string>
|
||||
<string name="add_new_category">Kategori baru</string>
|
||||
<string name="add">Tambah</string>
|
||||
<string name="enter_category_name">Masukkan nama kategori</string>
|
||||
<string name="save">Simpan</string>
|
||||
<string name="share">Bagikan</string>
|
||||
<string name="create_shortcut">Buat pintasan…</string>
|
||||
<string name="share_s">Bagikan %s</string>
|
||||
<string name="search">Cari</string>
|
||||
<string name="search_manga">Cari manga</string>
|
||||
<string name="manga_downloading_">Mengunduh…</string>
|
||||
<string name="processing_">Memproses…</string>
|
||||
<string name="download_complete">Diunduh</string>
|
||||
<string name="downloads">Unduhan</string>
|
||||
<string name="by_name">Nama</string>
|
||||
<string name="popular">Populer</string>
|
||||
<string name="updated">Diperbarui</string>
|
||||
<string name="newest">Terbaru</string>
|
||||
<string name="by_rating">Rating</string>
|
||||
<string name="sort_order">Urutan penyortiran</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="light">Terang</string>
|
||||
<string name="dark">Gelap</string>
|
||||
<string name="automatic">Ikuti sistem</string>
|
||||
<string name="pages">Halaman</string>
|
||||
<string name="clear">Bersihkan</string>
|
||||
<string name="remove">Hapus</string>
|
||||
<string name="_s_removed_from_history">\"%s\" dihapus dari riwayat</string>
|
||||
<string name="clear_history">Bersihkan riwayat</string>
|
||||
<string name="delete">Hapus</string>
|
||||
<string name="operation_not_supported">Tindakan ini tidak didukung</string>
|
||||
<string name="text_file_not_supported">Pilih antara berkas ZIP atau CBZ.</string>
|
||||
<string name="no_description">Tidak ada deskripsi</string>
|
||||
<string name="_s_deleted_from_local_storage">\"%s\" dihapus dari penyimpanan lokal</string>
|
||||
<string name="wait_for_loading_finish">Menunggu pemuatan selesai…</string>
|
||||
<string name="save_page">Simpan halaman</string>
|
||||
<string name="page_saved">Disimpan</string>
|
||||
<string name="share_image">Bagikan gambar</string>
|
||||
<string name="_import">Impor</string>
|
||||
<string name="history_and_cache">Riwayat dan tembolok</string>
|
||||
<string name="clear_pages_cache">Bersihkan tembolok halaman</string>
|
||||
<string name="cache">Tembolok</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="standard">Standar</string>
|
||||
<string name="grid_size">Ukuran kisi</string>
|
||||
<string name="search_on_s">Cari di %s</string>
|
||||
<string name="delete_manga">Hapus manga</string>
|
||||
<string name="text_delete_local_manga">Hapus \"%s\" dari perangkat secara permanen\?</string>
|
||||
<string name="reader_settings">Pengaturan pembaca</string>
|
||||
<string name="switch_pages">Ganti halaman</string>
|
||||
<string name="chapters">Bab</string>
|
||||
<string name="list">Daftar</string>
|
||||
<string name="detailed_list">Daftar terperinci</string>
|
||||
<string name="text_clear_history_prompt">Bersihkan semua riwayat baca secara permanen\?</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="read_mode">Mode baca</string>
|
||||
<string name="volume_buttons">Tombol volume</string>
|
||||
<string name="_continue">Lanjut</string>
|
||||
<string name="warning">Peringatan</string>
|
||||
<string name="network_consumption_warning">Ini mungkin akan mentransfer banyak data</string>
|
||||
<string name="dont_ask_again">Jangan tanya lagi</string>
|
||||
<string name="cancelling_">Membatalkan…</string>
|
||||
<string name="clear_thumbs_cache">Bersihkan tembolok keluku</string>
|
||||
<string name="clear_search_history">Bersihkan riwayat pencarian</string>
|
||||
<string name="search_history_cleared">Dibersihkan</string>
|
||||
<string name="domain">Domain</string>
|
||||
<string name="application_update">Periksa versi baru aplikasi</string>
|
||||
<string name="app_update_available">Versi baru aplikasi tersedia</string>
|
||||
<string name="open_in_browser">Buka di peramban web</string>
|
||||
<string name="save_manga">Simpan</string>
|
||||
<string name="notifications">Pemberitahuan</string>
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d dari %2$d diaktifkan</string>
|
||||
<string name="new_chapters">Bab baru</string>
|
||||
<string name="download">Unduh</string>
|
||||
<string name="read_from_start">Baca dari awal</string>
|
||||
<string name="restart">Mulai ulang</string>
|
||||
<string name="notifications_settings">Pengaturan pemberitahuan</string>
|
||||
<string name="notification_sound">Suara pemberitahuan</string>
|
||||
<string name="categories_">Kategori…</string>
|
||||
<string name="rename">Ubah Nama</string>
|
||||
<string name="category_delete_confirm">Hapus kategori \"%s\" dari favorit Anda\?
|
||||
\nSemua manga di situ akan hilang.</string>
|
||||
<string name="text_empty_holder_primary">Sepi juga di sini…</string>
|
||||
<string name="text_categories_holder">Anda bisa menggunakan kategori untuk mengelola favorit Anda. Tekan «+» untuk membuat kategori</string>
|
||||
<string name="text_history_holder_primary">Apa yang Anda baca akan ditampilkan di sini</string>
|
||||
<string name="text_history_holder_secondary">Cari apa untuk di baca di bilah samping.</string>
|
||||
<string name="text_local_holder_primary">Simpan sesuatu dulu</string>
|
||||
<string name="manga_shelf">Rak</string>
|
||||
<string name="recent_manga">Baru-baru ini</string>
|
||||
<string name="pages_animation">Animasi halaman</string>
|
||||
<string name="manga_save_location">Folder untuk unduhan</string>
|
||||
<string name="not_available">Tidak tersedia</string>
|
||||
<string name="cannot_find_available_storage">Tidak ada penyimpanan yang tersedia</string>
|
||||
<string name="other_storage">Penyimpanan lain</string>
|
||||
<string name="done">Selesai</string>
|
||||
<string name="all_favourites">Semua favorit</string>
|
||||
<string name="favourites_category_empty">Kategori kosong</string>
|
||||
<string name="read_later">Baca nanti</string>
|
||||
<string name="updates">Pembaruan</string>
|
||||
<string name="text_feed_holder">Bab baru dari apa yang Anda baca ditampilkan di sini</string>
|
||||
<string name="search_results">Hasil pencarian</string>
|
||||
<string name="related">Terkait</string>
|
||||
<string name="new_version_s">Versi baru: %s</string>
|
||||
<string name="size_s">Ukuran: %s</string>
|
||||
<string name="updates_feed_cleared">Dibersihkan</string>
|
||||
<string name="update">Pembaruan</string>
|
||||
<string name="track_sources">Mencari pembaruan</string>
|
||||
<string name="dont_check">Jangan periksa</string>
|
||||
<string name="wrong_password">Kata sandi salah</string>
|
||||
<string name="protect_application">Lindungi aplikasi</string>
|
||||
<string name="repeat_password">Ulangi kata sandi</string>
|
||||
<string name="update_check_failed">Tidak bisa mencari pembaruan</string>
|
||||
<string name="scale_mode">Mode skala</string>
|
||||
<string name="create_backup">Buat cadangan data</string>
|
||||
<string name="data_restored">Dipulihkan</string>
|
||||
<string name="report_github">Buat isu di GitHub</string>
|
||||
<string name="data_restored_success">Semua data dipulihkan</string>
|
||||
<string name="data_restored_with_errors">Data berhasil dipulihkan, tapi ada galat</string>
|
||||
<string name="backup_information">Anda bisa membuat cadangan riwayat dan favorit Anda dan memulihkannya</string>
|
||||
<string name="just_now">Baru saja</string>
|
||||
<string name="long_ago">Dulu kala</string>
|
||||
<string name="group">Kelompok</string>
|
||||
<string name="today">Hari ini</string>
|
||||
<string name="tap_to_try_again">Ketuk untuk coba lagi</string>
|
||||
<string name="reader_mode_hint">Konfigurasi yang dipilih akan diingat untuk manga ini</string>
|
||||
<string name="silent">Diam</string>
|
||||
<string name="captcha_required">CAPTCHA diperlukan</string>
|
||||
<string name="captcha_solve">Selesaikan</string>
|
||||
<string name="clear_cookies">Bersihkan kuki</string>
|
||||
<string name="cookies_cleared">Semua kuki telah dihapus</string>
|
||||
<string name="chapters_checking_progress">Memeriksa bab baru: %1$d dari %2$d</string>
|
||||
<string name="clear_feed">Bersihkan umpan</string>
|
||||
<string name="check_for_new_chapters">Periksa bab baru</string>
|
||||
<string name="auth_required">Masuk untuk melihat konten ini</string>
|
||||
<string name="_and_x_more">…dan %1$d lagi</string>
|
||||
<string name="next">Selanjutnya</string>
|
||||
<string name="protect_application_subtitle">Masukkan kata sandi untuk memulai aplikasi</string>
|
||||
<string name="confirm">Konfirmasi</string>
|
||||
<string name="search_only_on_s">Cari hanya di %s</string>
|
||||
<string name="text_clear_search_history_prompt">Hapus semua kueri pencarian baru-baru ini secara permanen\?</string>
|
||||
<string name="other">Lainnya</string>
|
||||
<string name="welcome">Selamat Datang</string>
|
||||
<string name="backup_saved">Cadangan disimpan</string>
|
||||
<string name="tracker_warning">Beberapa perangkat mempunyai perilaku sistem yang berbeda, yang mungkin akan merusak tugas latar belakang.</string>
|
||||
<string name="read_more">Baca lebih lanjut</string>
|
||||
<string name="text_downloads_holder">Tidak ada unduhan aktif</string>
|
||||
<string name="chapter_is_missing">Bab ini menghilang</string>
|
||||
<string name="about_app_translation">Terjemahan</string>
|
||||
<string name="about_feedback_4pda">Topik pada 4PDA</string>
|
||||
<string name="auth_not_supported_by">Masuk pada %s tidak didukung</string>
|
||||
<string name="text_clear_cookies_prompt">Anda akan keluar dari semua sumber</string>
|
||||
<string name="genres">Genre</string>
|
||||
<string name="state_finished">Selesai</string>
|
||||
<string name="state_ongoing">Sedang dibaca</string>
|
||||
<string name="date_format">Format tanggal</string>
|
||||
<string name="system_default">Standar</string>
|
||||
<string name="exclude_nsfw_from_history">Kecualikan manga NSFW dari riwayat</string>
|
||||
<string name="show_pages_numbers">Halaman bernomor</string>
|
||||
<string name="enabled_sources">Sumber yang digunakan</string>
|
||||
<string name="available_sources">Sumber yang tersedia</string>
|
||||
<string name="dynamic_theme">Tema dinamis</string>
|
||||
<string name="dynamic_theme_summary">Gunakan tema yang dibuat pada skema warna gambar latar belakang Anda</string>
|
||||
<string name="importing_progress">Mengimpor manga: %1$d dari %2$d</string>
|
||||
<string name="screenshots_policy">Kebijakan tangkapan layar</string>
|
||||
<string name="screenshots_allow">Bolehkan</string>
|
||||
<string name="screenshots_block_nsfw">Blokir pada NSFW</string>
|
||||
<string name="screenshots_block_all">Selalu blokir</string>
|
||||
<string name="suggestions">Saran</string>
|
||||
<string name="suggestions_enable">Aktifkan saran</string>
|
||||
<string name="suggestions_summary">Sarankan manga berdasarkan preferensi Anda</string>
|
||||
<string name="suggestions_info">Semua data dianalisis secara lokal di perangkat ini. Tidak ada transfer data personal ke layanan apa pun</string>
|
||||
<string name="text_suggestion_holder">Mulai membaca manga dan Anda akan mendapatkan saran yang dipersonalisasi</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Jangan menyarankan manga NSFW</string>
|
||||
<string name="always">Selalu</string>
|
||||
<string name="preload_pages">Muat ulang halaman</string>
|
||||
<string name="logged_in_as">Masuk sebagai %s</string>
|
||||
<string name="enabled">Diaktifkan</string>
|
||||
<string name="disabled">Dinonaktifkan</string>
|
||||
<string name="filter_load_error">Tidak bisa memuat daftar genre</string>
|
||||
<string name="reset_filter">Atur ulang filter</string>
|
||||
<string name="find_genre">Cari genre</string>
|
||||
<string name="onboard_text">Pilih bahasa manga yang ingin Anda baca. Anda bisa mengubahnya nanti di pengaturan.</string>
|
||||
<string name="never">Jangan Pernah</string>
|
||||
<string name="only_using_wifi">Hanya di Wi-Fi</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="various_languages">Berbagai bahasa</string>
|
||||
<string name="search_chapters">Cari bab</string>
|
||||
<string name="chapters_empty">Tidak ada bab di manga ini</string>
|
||||
<string name="percent_string_pattern">%1$s%%</string>
|
||||
<string name="appearance">Tampilan</string>
|
||||
<string name="content">Konten</string>
|
||||
<string name="suggestions_excluded_genres">Kecualikan genre</string>
|
||||
<string name="suggestions_excluded_genres_summary">Tentukan genre yang Anda tidak ingin lihat di saran</string>
|
||||
<string name="text_delete_local_manga_batch">Hapus yang dipilih dari perangkat secara permanen\?</string>
|
||||
<string name="removal_completed">Selesai menghapus</string>
|
||||
<string name="batch_manga_save_confirm">Apakah Anda yakin ingin mengunduh semua manga yang dipilih dengan semua babnya\? Tindakan ini akan memakan banyak data dan penyimpanan</string>
|
||||
<string name="parallel_downloads">Unduhan paralel</string>
|
||||
<string name="download_slowdown">Perlambatan unduhan</string>
|
||||
<string name="suggestions_updating">Saran diperbarui</string>
|
||||
<string name="download_slowdown_summary">Membantu menghidari pemblokiran alamat IP Anda</string>
|
||||
<string name="chapters_will_removed_background">Bab akan dihapus di latar belakang. Akan memakan beberapa waktu</string>
|
||||
<string name="hide">Sembunyikan</string>
|
||||
<string name="new_sources_text">Sumber manga baru tersedia</string>
|
||||
<string name="check_new_chapters_title">Periksa bab baru dan beri tahu tentang itu</string>
|
||||
<string name="show_notification_new_chapters_on">Anda akan menerima pemberitahuan tentang pembaruan manga yang Anda baca</string>
|
||||
<string name="show_notification_new_chapters_off">Anda tidak akan menerima pemberitahuan, tapi bab baru akan disorot di daftar</string>
|
||||
<string name="notifications_enable">Aktifkan pemberitahuan</string>
|
||||
<string name="name">Nama</string>
|
||||
<string name="edit">Sunting</string>
|
||||
<string name="edit_category">Sunting kategori</string>
|
||||
<string name="empty_favourite_categories">Tidak ada kategori favorit</string>
|
||||
<string name="bookmark_add">Tambah markah</string>
|
||||
<string name="bookmark_remove">Hapus markah</string>
|
||||
<string name="bookmarks">Markah</string>
|
||||
<string name="bookmark_removed">Markah dihapus</string>
|
||||
<string name="bookmark_added">Markah ditambahkan</string>
|
||||
<string name="undo">Urung</string>
|
||||
<string name="removed_from_history">Dihapus dari riwayat</string>
|
||||
<string name="default_mode">Mode standar</string>
|
||||
<string name="detect_reader_mode">Otomatis deteksi mode pembaca</string>
|
||||
<string name="detect_reader_mode_summary">Secara otomatis mendeteksi jika manga itu webtoon</string>
|
||||
<string name="create_category">Kategori baru</string>
|
||||
<string name="show_notification_app_update">Tampilkan pemberitahuan ketika versi baru tersedia</string>
|
||||
<string name="light_indicator">Indikator LED</string>
|
||||
<string name="vibration">Getaran</string>
|
||||
<string name="favourites_categories">Kategori favorit</string>
|
||||
<string name="remove_category">Hapus</string>
|
||||
<string name="clear_updates_feed">Bersihkan umpan pembaruan</string>
|
||||
<string name="right_to_left">Kanan ke kiri (←)</string>
|
||||
<string name="waiting_for_network">Menunggu jaringan…</string>
|
||||
<string name="rotate_screen">Putar layar</string>
|
||||
<string name="feed_will_update_soon">Pembaruan umpan akan dimulai</string>
|
||||
<string name="enter_password">Masukkan kata sandi</string>
|
||||
<string name="protect_application_summary">Tanya kata sandi ketika memulai Kotatsu</string>
|
||||
<string name="about">Tentang</string>
|
||||
<string name="check_for_updates">Periksa pembaruan</string>
|
||||
<string name="passwords_mismatch">Kata sandi tidak sama</string>
|
||||
<string name="app_version">Versi %s</string>
|
||||
<string name="internal_storage">Penyimpanan internal</string>
|
||||
<string name="external_storage">Penyimpanan eksternal</string>
|
||||
<string name="error">Galat</string>
|
||||
<string name="checking_for_updates">Memeriksa pembaruan…</string>
|
||||
<string name="no_update_available">Tidak ada pembaruan yang tersedia</string>
|
||||
<string name="black_dark_theme_summary">Menggunakan daya lebih sedikit pada layar AMOLED</string>
|
||||
<string name="black_dark_theme">Hitam</string>
|
||||
<string name="backup_restore">Pencadangan dan pemulihan</string>
|
||||
<string name="yesterday">Kemarin</string>
|
||||
<string name="restore_backup">Pulihkan dari cadangan</string>
|
||||
<string name="file_not_found">Berkas tidak ditemukan</string>
|
||||
<string name="password_length_hint">Kata sandi harus 4 karakter atau lebih</string>
|
||||
<string name="preparing_">Mempersiapkan…</string>
|
||||
<string name="text_clear_updates_feed_prompt">Bersihkan semua riwayat pembaruan secara permanen\?</string>
|
||||
<string name="sign_in">Masuk</string>
|
||||
<string name="default_s">Standar: %s</string>
|
||||
<string name="chapter_is_missing_text">Unduh atau baca bab yang hilang ini secara daring.</string>
|
||||
<string name="about_app_translation_summary">Terjemahkan aplikasi ini</string>
|
||||
<string name="about_feedback">Umpan Balik</string>
|
||||
<string name="error_empty_name">Anda harus memasukkan sebuah nama</string>
|
||||
<string name="taps_on_edges">Tekan di tepi</string>
|
||||
<string name="large_manga_save_confirm">Manga ini memiliki %s. Simpan semuanya\?</string>
|
||||
<string name="zoom_mode_fit_center">Cocokkan tengah</string>
|
||||
<string name="zoom_mode_fit_height">Cocokkan dengan tinggi</string>
|
||||
<string name="zoom_mode_fit_width">Cocokkan dengan lebar</string>
|
||||
<string name="reverse">Balik</string>
|
||||
<string name="queued">Diantrikan</string>
|
||||
<string name="auth_complete">Berhasil Diotorisasi</string>
|
||||
</resources>
|
||||
@@ -294,4 +294,8 @@
|
||||
<string name="detect_reader_mode">Modalità di lettura a rilevamento automatico</string>
|
||||
<string name="dns_over_https">DNS su HTTPS</string>
|
||||
<string name="detect_reader_mode_summary">Rileva automaticamente se il manga è un webtoon</string>
|
||||
<string name="disable_battery_optimization">Disattiva l\'ottimizzazione della batteria</string>
|
||||
<string name="disable_battery_optimization_summary">Contribuisce ai controlli di aggiornamento in sfondo</string>
|
||||
<string name="crash_text">Qualcosa è andato storto. Si prega di inviare una segnalazione di bug agli sviluppatori per aiutarci a risolvere il problema.</string>
|
||||
<string name="send">Invia</string>
|
||||
</resources>
|
||||
@@ -294,4 +294,8 @@
|
||||
<string name="detect_reader_mode">リーダーモードの自動検出</string>
|
||||
<string name="default_mode">デフォルトモード</string>
|
||||
<string name="detect_reader_mode_summary">マンガがウェブトゥーンかどうかを自動判定</string>
|
||||
<string name="disable_battery_optimization">バッテリー最適化の無効化</string>
|
||||
<string name="disable_battery_optimization_summary">バックグラウンドの更新チェックを支援</string>
|
||||
<string name="crash_text">何か問題が発生しました。開発者にバグレポートを提出し、解決にご協力ください。</string>
|
||||
<string name="send">送信</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user