Compare commits

...

63 Commits
v0.1 ... v0.3

Author SHA1 Message Date
Koitharu
0d0982b244 Check if in-app update allowed 2020-04-19 08:43:45 +03:00
Koitharu
ef4dd82e92 Fix TextInputDialog keyboard behavior 2020-04-19 08:06:24 +03:00
Koitharu
bc825681a8 Small ui fixes 2020-04-18 20:24:19 +03:00
Koitharu
da6204f44f Remove unused resources 2020-04-18 19:56:12 +03:00
Koitharu
10c68bdd72 Update readme 2020-04-18 17:33:27 +03:00
Koitharu
b1e90dde8f Optimize page loading 2020-04-18 12:20:18 +03:00
Koitharu
e0d45961f8 Add some animations 2020-04-18 11:19:20 +03:00
Koitharu
b732a220f6 Merge remote-tracking branch 'origin/feature/appwidget' into devel 2020-04-12 12:44:54 +03:00
Koitharu
582adae11f Manage favourites categories 2020-04-12 12:40:40 +03:00
Koitharu
3014ebdfd4 Notification settings for pre-Oreo android 2020-04-12 09:58:20 +03:00
Koitharu
12b13f98f8 Migrate to WorkManager 2020-04-11 11:39:40 +03:00
Koitharu
c13c43c616 Recent manga app widget 2020-04-10 20:13:17 +03:00
Koitharu
ab1eacea3f Shelf app widget 2020-04-10 19:56:51 +03:00
Koitharu
ac4b97928a Merge branch 'devel' of https://github.com/nv95/Kotatsu into devel 2020-04-10 17:14:40 +03:00
Koitharu
aa8281678b Update settings 2020-04-10 17:14:33 +03:00
Koitharu
0be4f56538 Refactor shortcut helper 2020-04-08 20:30:44 +03:00
Koitharu
679c06557e Change notification icon 2020-04-08 20:15:29 +03:00
Koitharu
1d387709f2 Cleanup code 2020-04-05 20:40:58 +03:00
Koitharu
a78774d10e Optimize pages thumbnails 2020-04-05 13:04:22 +03:00
Koitharu
390639e9e3 Fix recycled bitmap crash 2020-04-05 12:58:06 +03:00
Koitharu
b98ec2199d Crash info dialog 2020-04-05 12:46:05 +03:00
Koitharu
8b28f1cd74 Small UI fixes 2020-04-03 21:20:41 +03:00
Koitharu
904b78a01e Optimize images in lists 2020-04-03 20:59:34 +03:00
Koitharu
a774d2d915 Read from start quick action 2020-04-03 20:45:18 +03:00
Koitharu
9d19b5fec0 Fix grouple genres list 2020-04-03 20:34:14 +03:00
Koitharu
b6c0f3ca8c Small refactor 2020-04-03 20:24:46 +03:00
Koitharu
e06cb1230f Update dependencies 2020-04-03 19:54:18 +03:00
Koitharu
1720fde4c4 Small fix webtoon reader 2020-04-02 21:59:41 +03:00
Koitharu
4c3dbe1643 Update notification enhancement 2020-04-02 20:39:15 +03:00
Koitharu
3f31bd5ad1 Tracker enhancements 2020-04-02 20:24:46 +03:00
Koitharu
3a79b4667b Merge branch 'master' into devel 2020-04-02 20:10:52 +03:00
Koitharu
de49877178 Fix crash and foreign key issue 2020-03-30 20:55:53 +03:00
Koitharu
65e92fa206 Fix sources preference summary 2020-03-29 21:18:32 +03:00
Koitharu
9cb181d53e Show new chapters on details 2020-03-29 21:08:45 +03:00
Koitharu
a2d4a63eb1 Tracker notifications option in settings 2020-03-29 20:48:53 +03:00
Koitharu
c4f712be3a Improve download notification 2020-03-29 20:41:37 +03:00
Koitharu
9e8367e45e Increase database version 2020-03-29 20:24:06 +03:00
Koitharu
fa2d1de2f2 Merge branch 'San4ito-patch-1' into devel 2020-03-29 18:05:48 +03:00
Koitharu
f8f4573486 Merge branch 'patch-1' of https://github.com/San4ito/Kotatsu into San4ito-patch-1 2020-03-29 18:05:30 +03:00
Koitharu
f15f0ce769 Merge branch 'master' into devel 2020-03-29 18:03:58 +03:00
Koitharu
450daf17fd Fix foreign key error 2020-03-29 17:57:04 +03:00
Koitharu
aad26d24ec Tracking manga updates 2020-03-29 17:33:44 +03:00
Koitharu
80c8344f8d Manga tracking job service 2020-03-29 13:32:25 +03:00
Koitharu
44b23d0b69 Merge branch 'master' into devel 2020-03-29 11:16:45 +03:00
Koitharu
7ee486e4f2 Fix version code 2020-03-29 10:58:24 +03:00
Koitharu
f230f2d198 Update database scheme: tracks table 2020-03-29 10:40:49 +03:00
San4ito
cf50b608a7 Убрал небольшие опечатки
Думаю, так должно быть лучше.
2020-03-28 22:14:21 +03:00
Koitharu
1314c601b2 Simplify settings 2020-03-28 19:40:43 +02:00
Koitharu
c5970c5606 Confirm large manga downloading 2020-03-28 19:01:50 +02:00
Koitharu
85b18d118b Store page scroll position in history 2020-03-28 18:49:01 +02:00
Koitharu
e7a150bd9a Replace history with remote manga when delete local 2020-03-28 12:46:57 +02:00
Koitharu
2c66edda68 Update database schema: foreign keys and indices 2020-03-28 12:28:03 +02:00
Koitharu
1a93cc228d Update dependencies 2020-03-23 18:14:27 +02:00
Koitharu
798ae6aeb7 Fix pages max scale 2020-03-22 17:35:42 +02:00
Koitharu
418d0247f5 Option to hide manga source 2020-03-19 16:52:16 +02:00
Koitharu
db0ee268f9 Browser activity 2020-03-19 14:31:25 +02:00
Koitharu
032d671c38 Add Chucker for debug 2020-03-19 13:39:19 +02:00
Koitharu
127978d3d7 Decrease app update checking interval 2020-03-19 12:53:14 +02:00
Koitharu
fddc3e41cf Remove shortcut if local manga removed 2020-03-19 12:47:36 +02:00
Koitharu
e0e6f0dab4 DesuMe parser 2020-03-18 20:33:57 +02:00
Koitharu
beaa825a9f Add UserAgent 2020-03-18 19:22:10 +02:00
Koitharu
cae27dda05 Fix all parsers issues 2020-03-18 19:12:27 +02:00
Koitharu
0d041e9a0a Fix appending saved chapters 2020-03-17 18:15:15 +02:00
192 changed files with 3502 additions and 910 deletions

120
.idea/codeStyles generated
View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
</project>

186
.idea/codeStyles/Project.xml generated Executable file
View File

@@ -0,0 +1,186 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
</value>
</option>
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
<option name="MANIFEST_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
<option name="OTHER_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Shell Script">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Executable file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

8
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel>
<module name="Kotatsu.app" target="1.8" />
</bytecodeTargetLevel>
</component>
</project>

View File

@@ -1,6 +1,8 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="admin"> <dictionary name="admin">
<words> <words>
<w>chucker</w>
<w>desu</w>
<w>koin</w> <w>koin</w>
<w>kotatsu</w> <w>kotatsu</w>
<w>manga</w> <w>manga</w>

1
.idea/gradle.xml generated
View File

@@ -15,6 +15,7 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
</profile>
</component>

View File

@@ -21,5 +21,15 @@
<option name="name" value="Google" /> <option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" /> <option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
</remote-repository>
</component> </component>
</project> </project>

View File

@@ -2,13 +2,11 @@
Kotatsu is a free and open source manga reader for Android. Kotatsu is a free and open source manga reader for Android.
![Android minSdk](https://img.shields.io/badge/android-5.0+-brightgreen) ![GitHub top language](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) ![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669)
### Download ### Download
Latest unstable build: [get here](https://github.com/nv95/Kotatsu/releases/latest) Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
Stable release: _Coming soon_
### Main Features ### Main Features
@@ -20,9 +18,6 @@ Stable release: _Coming soon_
* Tablet-optimized modern UI * Tablet-optimized modern UI
* Reading third-party comics from CBZ * Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
### Coming Features
* Checking for new chapters * Checking for new chapters
### Screenshots ### Screenshots

View File

@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
def gitCommits = 'git rev-list --all --count'.execute([], rootDir).text.trim().toInteger() def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim() def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
android { android {
@@ -15,7 +15,7 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName '0.1' versionName '0.3'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\"" buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
@@ -61,35 +61,40 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'androidx.core:core-ktx:1.3.0-alpha02' implementation 'androidx.core:core-ktx:1.3.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.2.2' implementation 'androidx.fragment:fragment-ktx:1.2.4'
implementation 'androidx.appcompat:appcompat:1.2.0-alpha03' implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'com.google.android.material:material:1.2.0-alpha05' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'androidx.room:room-runtime:2.2.4' implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.4' implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.4' kapt 'androidx.room:room-compiler:2.2.5'
implementation 'com.github.moxy-community:moxy:2.1.1' implementation 'com.github.moxy-community:moxy:2.1.2'
implementation 'com.github.moxy-community:moxy-androidx:2.1.1' implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
implementation 'com.github.moxy-community:moxy-material:2.1.1' implementation 'com.github.moxy-community:moxy-material:2.1.2'
implementation 'com.github.moxy-community:moxy-ktx:2.1.1' implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.1' kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.4.0' implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okio:okio:2.4.3' implementation 'com.squareup.okio:okio:2.5.0'
implementation 'org.jsoup:jsoup:1.12.2' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.1.3' implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.9.5' implementation 'io.coil-kt:coil:0.9.5'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722'
} }

1
app/libs/.gitkeep Normal file
View File

@@ -0,0 +1 @@

Binary file not shown.

View File

@@ -1,13 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.koitharu.kotatsu"> package="org.koitharu.kotatsu">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:name="org.koitharu.kotatsu.KotatsuApp" android:name="org.koitharu.kotatsu.KotatsuApp"
@@ -18,7 +21,8 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity android:name=".ui.main.MainActivity"> <activity android:name=".ui.main.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -42,16 +46,38 @@
android:label="@string/settings" /> android:label="@string/settings" />
<activity <activity
android:name=".ui.reader.SimpleSettingsActivity" android:name=".ui.reader.SimpleSettingsActivity"
android:label="@string/settings" /> android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ui.browser.BrowserActivity" />
<activity
android:name=".ui.utils.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:label="@string/favourites_categories" />
<service <service
android:name=".ui.download.DownloadService" android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" /> <service android:name=".ui.settings.AppUpdateService" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".ui.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider <provider
android:name=".ui.search.MangaSuggestionsProvider" android:name=".ui.search.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider" /> android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.files" android:authorities="${applicationId}.files"
@@ -61,6 +87,23 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" /> android:resource="@xml/filepaths" />
</provider> </provider>
<receiver android:name=".ui.widget.shelf.ShelfWidgetProvider"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_shelf" />
</receiver>
<receiver android:name=".ui.widget.recent.RecentWidgetProvider"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" />
</receiver>
</application> </application>

View File

@@ -6,20 +6,29 @@ import androidx.room.Room
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import coil.util.CoilUtils import coil.util.CoilUtils
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.local.CbzFetcher import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -29,11 +38,22 @@ class KotatsuApp : Application() {
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext)) PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
} }
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
ChuckerCollector(applicationContext)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initKoin() initKoin()
initCoil() initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
if (BuildConfig.DEBUG) {
initErrorHandler()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme) AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
val widgetUpdater = WidgetUpdater(applicationContext)
FavouritesRepository.subscribe(widgetUpdater)
HistoryRepository.subscribe(widgetUpdater)
} }
private fun initKoin() { private fun initKoin() {
@@ -77,13 +97,22 @@ class KotatsuApp : Application() {
}) })
} }
private fun initErrorHandler() {
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
chuckerCollector.onError("CRASH", e)
exceptionHandler?.uncaughtException(t, e)
}
}
private fun okHttp() = OkHttpClient.Builder().apply { private fun okHttp() = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS) connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS) writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar) cookieJar(cookieJar)
addInterceptor(UserAgentInterceptor)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
addInterceptor(OkHttpProfilerInterceptor()) addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
} }
} }
@@ -91,5 +120,5 @@ class KotatsuApp : Application() {
applicationContext, applicationContext,
MangaDatabase::class.java, MangaDatabase::class.java,
"kotatsu-db" "kotatsu-db"
) ).addMigrations(Migration1To2, Migration2To3, Migration3To4)
} }

View File

@@ -17,4 +17,7 @@ abstract class FavouriteCategoriesDao {
@Query("DELETE FROM favourite_categories WHERE category_id = :id") @Query("DELETE FROM favourite_categories WHERE category_id = :id")
abstract suspend fun delete(id: Long) abstract suspend fun delete(id: Long)
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String)
} }

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db
import androidx.room.* import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteManga import org.koitharu.kotatsu.core.db.entity.FavouriteManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao @Dao
abstract class FavouritesDao { abstract class FavouritesDao {
@@ -11,6 +12,9 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY :orderBy LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY :orderBy LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga> abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Transaction @Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id") @Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga? abstract suspend fun find(id: Long): FavouriteManga?

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db
import androidx.room.* import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.HistoryEntity import org.koitharu.kotatsu.core.db.entity.HistoryEntity
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao @Dao
@@ -15,6 +16,9 @@ abstract class HistoryDao {
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga> abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Query("SELECT * FROM history WHERE manga_id = :id") @Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity? abstract suspend fun find(id: Long): HistoryEntity?
@@ -24,19 +28,20 @@ abstract class HistoryDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long abstract suspend fun insert(entity: HistoryEntity): Long
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, updated_at = :updatedAt WHERE manga_id = :mangaId") @Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, updatedAt: Long): Int abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
@Query("DELETE FROM history WHERE manga_id = :mangaId") @Query("DELETE FROM history WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long) abstract suspend fun delete(mangaId: Long)
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.updatedAt) suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
@Transaction @Transaction
open suspend fun upsert(entity: HistoryEntity) { open suspend fun upsert(entity: HistoryEntity): Boolean {
if (update(entity) == 0) { return if (update(entity) == 0) {
insert(entity) insert(entity)
} true
} else false
} }
} }

View File

@@ -7,20 +7,22 @@ import org.koitharu.kotatsu.core.db.entity.*
@Database( @Database(
entities = [ entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
], version = 1 ], version = 4
) )
abstract class MangaDatabase : RoomDatabase() { abstract class MangaDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao abstract val historyDao: HistoryDao
abstract fun tagsDao(): TagsDao abstract val tagsDao: TagsDao
abstract fun mangaDao(): MangaDao abstract val mangaDao: MangaDao
abstract fun favouritesDao(): FavouritesDao abstract val favouritesDao: FavouritesDao
abstract fun preferencesDao(): PreferencesDao abstract val preferencesDao: PreferencesDao
abstract fun favouriteCategoriesDao(): FavouriteCategoriesDao abstract val favouriteCategoriesDao: FavouriteCategoriesDao
abstract val tracksDao: TracksDao
} }

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackEntity
@Dao
abstract class TracksDao {
@Query("SELECT * FROM tracks")
abstract suspend fun findAll(): List<TrackEntity>
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): TrackEntity?
@Query("DELETE FROM tracks")
abstract suspend fun clear()
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: TrackEntity): Long
@Update
abstract suspend fun update(entity: TrackEntity): Int
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
@Transaction
open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) {
insert(entity)
}
}
}

View File

@@ -2,10 +2,26 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(tableName = "favourites", primaryKeys = ["manga_id", "category_id"]) @Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = FavouriteCategoryEntity::class,
parentColumns = ["category_id"],
childColumns = ["category_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class FavouriteEntity( data class FavouriteEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id") val categoryId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long @ColumnInfo(name = "created_at") val createdAt: Long
) )

View File

@@ -2,24 +2,36 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import java.util.* import java.util.*
@Entity(tableName = "history") @Entity(
tableName = "history", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class HistoryEntity( data class HistoryEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long, @ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long, @ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int @ColumnInfo(name = "page") val page: Int,
@ColumnInfo(name = "scroll") val scroll: Float
) { ) {
fun toMangaHistory() = MangaHistory( fun toMangaHistory() = MangaHistory(
createdAt = Date(createdAt), createdAt = Date(createdAt),
updatedAt = Date(updatedAt), updatedAt = Date(updatedAt),
chapterId = chapterId, chapterId = chapterId,
page = page page = page,
) scroll = scroll
)
} }

View File

@@ -2,9 +2,18 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "preferences") @Entity(
tableName = "preferences", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)]
)
data class MangaPrefsEntity( data class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,

View File

@@ -2,9 +2,25 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"]) @Entity(
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = TagEntity::class,
parentColumns = ["tag_id"],
childColumns = ["tag_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class MangaTagsEntity( data class MangaTagsEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id") val tagId: Long @ColumnInfo(name = "tag_id", index = true) val tagId: Long
) )

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "tracks", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class TrackEntity (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
@ColumnInfo(name = "last_chapter_id") val lastChapterId: Long,
@ColumnInfo(name = "chapters_new") val newChapters: Int,
@ColumnInfo(name = "last_check") val lastCheck: Long,
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
)

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration1To2 : Migration(1, 2) {
/**
* Adding foreign keys
*/
override fun migrate(database: SupportSQLiteDatabase) {
/* manga_tags */
database.execSQL(
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, tag_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
database.execSQL("DROP TABLE manga_tags")
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
/* favourites */
database.execSQL(
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, category_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
database.execSQL("DROP TABLE favourites")
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
/* history */
database.execSQL(
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
database.execSQL("DROP TABLE history")
database.execSQL("ALTER TABLE history_tmp RENAME TO history")
/* preferences */
database.execSQL(
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
" PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
database.execSQL("DROP TABLE preferences")
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration2To3 : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration3To4 : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

View File

@@ -10,10 +10,4 @@ data class AppVersion(
val url: String, val url: String,
val apkSize: Long, val apkSize: Long,
val apkUrl: String val apkUrl: String
) : Parcelable { ) : Parcelable
fun isGreaterThen(version: String) {
val thisParts = name.substringBeforeLast('-').split('.')
val parts = version.substringBeforeLast('-').split('.')
}
}

View File

@@ -15,6 +15,7 @@ import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> { class CbzFetcher : Fetcher<Uri> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch( override suspend fun fetch(
pool: BitmapPool, pool: BitmapPool,
data: Uri, data: Uri,

View File

@@ -37,7 +37,7 @@ class SetCookieCache : CookieCache {
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator() override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
private inner class SetCookieCacheIterator internal constructor() : MutableIterator<Cookie> { private inner class SetCookieCacheIterator() : MutableIterator<Cookie> {
private val iterator = cookies.iterator() private val iterator = cookies.iterator()

View File

@@ -41,27 +41,24 @@ class SharedPrefsCookiePersistor(private val sharedPreferences: SharedPreference
return cookies return cookies
} }
@SuppressLint("ApplySharedPref")
override fun saveAll(cookies: Collection<Cookie>) { override fun saveAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
for (cookie in cookies) { for (cookie in cookies) {
editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie)) editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie))
} }
editor.commit() editor.apply()
} }
@SuppressLint("ApplySharedPref")
override fun removeAll(cookies: Collection<Cookie>) { override fun removeAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
for (cookie in cookies) { for (cookie in cookies) {
editor.remove(createCookieKey(cookie)) editor.remove(createCookieKey(cookie))
} }
editor.commit() editor.apply()
} }
@SuppressLint("ApplySharedPref")
override fun clear() { override fun clear() {
sharedPreferences.edit().clear().commit() sharedPreferences.edit().clear().apply()
} }
private companion object { private companion object {

View File

@@ -9,5 +9,6 @@ data class MangaHistory(
val createdAt: Date, val createdAt: Date,
val updatedAt: Date, val updatedAt: Date,
val chapterId: Long, val chapterId: Long,
val page: Int val page: Int,
val scroll: Float
) : Parcelable ) : Parcelable

View File

@@ -18,6 +18,7 @@ enum class MangaSource(
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java), MINTMANGA("MintManga", "ru", MintMangaRepository::class.java),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java), SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java),
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java), MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java),
DESUME("Desu.me", "ru", DesuMeRepository::class.java),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java), HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java),
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java) YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java)
} }

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.util.*
@Parcelize
data class MangaTracking (
val manga: Manga,
val knownChaptersCount: Int,
val lastChapterId: Long,
val lastNotifiedChapterId: Long,
val lastCheck: Date?
): Parcelable

View File

@@ -38,6 +38,7 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
getFromFile(Uri.parse(manga.url).toFile()) getFromFile(Uri.parse(manga.url).toFile())
} else manga } else manga
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val file = Uri.parse(chapter.url).toFile() val file = Uri.parse(chapter.url).toFile()
val zip = ZipFile(file) val zip = ZipFile(file)
@@ -104,6 +105,16 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
} }
} }
fun getRemoteManga(localManga: Manga): Manga? {
val file = safe {
Uri.parse(localManga.url).toFile()
} ?: return null
val zip = ZipFile(file)
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null
return index.getMangaInfo()
}
private fun zipUri(file: File, entryName: String) = private fun zipUri(file: File, entryName: String) =
Uri.fromParts("cbz", file.path, entryName).toString() Uri.fromParts("cbz", file.path, entryName).toString()

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.os.Build
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.util.*
@SuppressLint("ConstantLocale")
object UserAgentInterceptor : Interceptor {
private val userAgent = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language
)
override fun intercept(chain: Interceptor.Chain) = chain.proceed(
chain.request().newBuilder()
.header("User-Agent", userAgent)
.build()
)
}

View File

@@ -93,7 +93,7 @@ abstract class ChanRepository : RemoteMangaRepository() {
val json = data.substring(pos).substringAfter('[').substringBefore(';') val json = data.substring(pos).substringAfter('[').substringBefore(';')
.substringBeforeLast(']') .substringBeforeLast(']')
return json.split(",").mapNotNull { return json.split(",").mapNotNull {
it.trim().removeSurrounding('"').takeUnless(String::isBlank) it.trim().removeSurrounding('"','\'').takeUnless(String::isBlank)
}.map { url -> }.map { url ->
MangaPage( MangaPage(
id = url.longHashCode(), id = url.longHashCode(),

View File

@@ -0,0 +1,137 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
class DesuMeRepository : RemoteMangaRepository() {
override val source = MangaSource.DESUME
override val sortOrders = setOf(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL
)
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
val domain = conf.getDomain(DOMAIN)
val url = buildString {
append("https://")
append(domain)
append("/manga/api/?limit=20&order=")
append(getSortKey(sortOrder))
append("&page=")
append((offset / 20) + 1)
if (tag != null) {
append("&genres=")
append(tag.key)
}
if (query != null) {
append("&search=")
append(query)
}
}
val json = loaderContext.httpGet(url).parseJson().getJSONArray("response")
?: throw ParseException("Invalid response")
val total = json.length()
val list = ArrayList<Manga>(total)
for (i in 0 until total) {
val jo = json.getJSONObject(i)
val cover = jo.getJSONObject("image")
list += Manga(
url = jo.getString("url"),
source = MangaSource.DESUME,
title = jo.getString("russian"),
altTitle = jo.getString("name"),
coverUrl = cover.getString("preview"),
largeCoverUrl = cover.getString("original"),
state = when {
jo.getInt("ongoing") == 1 -> MangaState.ONGOING
else -> null
},
rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f),
id = ID_MASK + jo.getLong("id"),
description = jo.getString("description")
)
}
return list
}
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(DOMAIN)
val url = "https://$domain/manga/api/${manga.id - ID_MASK}"
val json = loaderContext.httpGet(url).parseJson().getJSONObject("response")
?: throw ParseException("Invalid response")
return manga.copy(
tags = json.getJSONArray("genres").map {
MangaTag(
key = it.getString("text"),
title = it.getString("russian"),
source = manga.source
)
}.toSet(),
description = json.getString("description"),
chapters = json.getJSONObject("chapters").getJSONArray("list").mapIndexed { i, it ->
val chid = it.getLong("id")
MangaChapter(
id = ID_MASK + chid,
source = manga.source,
url = "$url/chapter/$chid",
name = it.optString("title", "${manga.title} #${it.getDouble("ch")}"),
number = i + 1
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val json = loaderContext.httpGet(chapter.url).parseJson().getJSONObject("response")
?: throw ParseException("Invalid response")
return json.getJSONObject("pages").getJSONArray("list").map {
MangaPage(
id = it.getLong("id"),
source = chapter.source,
url = it.getString("img")
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val domain = conf.getDomain(DOMAIN)
val doc = loaderContext.httpGet("https://$domain/manga/").parseHtml()
val root = doc.body().getElementById("animeFilter").selectFirst(".catalog-genres")
return root.select("li").map {
MangaTag(
source = source,
key = it.selectFirst("input").attr("data-genre"),
title = it.selectFirst("label").text()
)
}.toSet()
}
override fun onCreatePreferences() = setOf(R.string.key_parser_domain)
private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder) {
SortOrder.ALPHABETICAL -> "name"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "updated"
SortOrder.NEWEST -> "id"
else -> "updated"
}
private companion object {
private const val ID_MASK = 1000
private const val DOMAIN = "desu.me"
}
}

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class GroupleRepository : RemoteMangaRepository() { abstract class GroupleRepository : RemoteMangaRepository() {
protected abstract val defaultDomain: String protected abstract val defaultDomain: String
@@ -20,7 +20,7 @@ abstract class GroupleRepository : RemoteMangaRepository() {
offset: Int, offset: Int,
query: String?, query: String?,
sortOrder: SortOrder?, sortOrder: SortOrder?,
tag: MangaTag? tag: MangaTag?,
): List<Manga> { ): List<Manga> {
val domain = conf.getDomain(defaultDomain) val domain = conf.getDomain(defaultDomain)
val doc = when { val doc = when {
@@ -28,7 +28,8 @@ abstract class GroupleRepository : RemoteMangaRepository() {
"https://$domain/search", "https://$domain/search",
mapOf("q" to query, "offset" to offset.toString()) mapOf("q" to query, "offset" to offset.toString())
) )
tag == null -> loaderContext.httpGet("https://$domain/list?sortType=${getSortKey(sortOrder)}&offset=$offset") tag == null -> loaderContext.httpGet("https://$domain/list?sortType=${getSortKey(
sortOrder)}&offset=$offset")
else -> loaderContext.httpGet( else -> loaderContext.httpGet(
"https://$domain/list/genre/${tag.key}?sortType=${getSortKey( "https://$domain/list/genre/${tag.key}?sortType=${getSortKey(
sortOrder sortOrder
@@ -85,12 +86,22 @@ abstract class GroupleRepository : RemoteMangaRepository() {
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain) val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml() val doc = loaderContext.httpGet(manga.url).parseHtml()
val root = doc.body().getElementById("mangaBox") ?: throw ParseException("Cannot find root") val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent")
?: throw ParseException("Cannot find root")
return manga.copy( return manga.copy(
description = root.selectFirst("div.manga-description")?.html(), description = root.selectFirst("div.manga-description")?.html(),
largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr( largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr(
"data-full" "data-full"
), ),
tags = manga.tags + root.select("div.subject-meta").select("span.elem_genre ")
.mapNotNull {
val a = it.selectFirst("a.element-link") ?: return@mapNotNull null
MangaTag(
title = a.text(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
},
chapters = root.selectFirst("div.chapters-link")?.selectFirst("table") chapters = root.selectFirst("div.chapters-link")?.selectFirst("table")
?.select("a")?.asReversed()?.mapIndexedNotNull { i, a -> ?.select("a")?.asReversed()?.mapIndexedNotNull { i, a ->
val href = val href =

View File

@@ -4,6 +4,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain import org.koitharu.kotatsu.utils.ext.withDomain
@@ -18,22 +19,27 @@ class HenChanRepository : ChanRepository() {
val doc = loaderContext.httpGet(manga.url).parseHtml() val doc = loaderContext.httpGet(manga.url).parseHtml()
val root = val root =
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root") doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
val readLink = manga.url.replace("manga", "online")
return manga.copy( return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"), description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.attr("src")?.withDomain(domain), largeCoverUrl = root.getElementById("cover")?.attr("src")?.withDomain(domain),
chapters = root.getElementById("right").select("table.table_cha").flatMap { table -> tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.map {
table.select("div.manga2") val a = it.children().last()
}.mapNotNull { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a -> MangaTag(
val href = a.attr("href") title = a.text(),
?.withDomain(domain) ?: return@mapIndexedNotNull null key = a.attr("href").substringAfterLast('/'),
MangaChapter(
id = href.longHashCode(),
name = a.text().trim(),
number = i + 1,
url = href,
source = source source = source
) )
} }?.toSet() ?: manga.tags,
chapters = listOf(
MangaChapter(
id = readLink.longHashCode(),
url = readLink,
source = source,
number = 1,
name = manga.title
)
)
) )
} }
} }

View File

@@ -1,9 +1,39 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain
class YaoiChanRepository : ChanRepository() { class YaoiChanRepository : ChanRepository() {
override val source = MangaSource.YAOICHAN override val source = MangaSource.YAOICHAN
override val defaultDomain = "yaoi-chan.me" override val defaultDomain = "yaoi-chan.me"
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml()
val root =
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.attr("src")?.withDomain(domain),
chapters = root.select("table.table_cha").flatMap { table ->
table.select("div.manga")
}.mapNotNull { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a ->
val href = a.attr("href")
?.withDomain(domain) ?: return@mapIndexedNotNull null
MangaChapter(
id = href.longHashCode(),
name = a.text().trim(),
number = i + 1,
url = href,
source = source
)
}
)
}
} }

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -52,6 +53,31 @@ class AppSettings private constructor(resources: Resources, private val prefs: S
0L 0L
) )
val trackerNotifications by BoolPreferenceDelegate(
resources.getString(R.string.key_tracker_notifications),
true
)
var notificationSound by StringPreferenceDelegate(
resources.getString(R.string.key_notifications_sound),
Settings.System.DEFAULT_NOTIFICATION_URI.toString()
)
val notificationVibrate by BoolPreferenceDelegate(
resources.getString(R.string.key_notifications_vibrate),
false
)
val notificationLight by BoolPreferenceDelegate(
resources.getString(R.string.key_notifications_light),
true
)
val readerAnimation by BoolPreferenceDelegate(
resources.getString(R.string.key_reader_animation),
false
)
private var sourcesOrderStr by NullableStringPreferenceDelegate(resources.getString(R.string.key_sources_order)) private var sourcesOrderStr by NullableStringPreferenceDelegate(resources.getString(R.string.key_sources_order))
var sourcesOrder: List<Int> var sourcesOrder: List<Int>
@@ -60,6 +86,8 @@ class AppSettings private constructor(resources: Resources, private val prefs: S
sourcesOrderStr = value.joinToString("|") sourcesOrderStr = value.joinToString("|")
} }
var hiddenSources by StringSetPreferenceDelegate(resources.getString(R.string.key_sources_hidden))
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain package org.koitharu.kotatsu.domain
import androidx.room.withTransaction
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -13,24 +14,33 @@ class MangaDataRepository : KoinComponent {
private val db: MangaDatabase by inject() private val db: MangaDatabase by inject()
suspend fun savePreferences(mangaId: Long, mode: ReaderMode) { suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
db.preferencesDao().upsert( val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
MangaPrefsEntity( db.withTransaction {
mangaId = mangaId, db.tagsDao.upsert(tags)
mode = mode.id db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.preferencesDao.upsert(
MangaPrefsEntity(
mangaId = manga.id,
mode = mode.id
)
) )
) }
} }
suspend fun getReaderMode(mangaId: Long): ReaderMode? { suspend fun getReaderMode(mangaId: Long): ReaderMode? {
return db.preferencesDao().find(mangaId)?.let { ReaderMode.valueOf(it.mode) } return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
} }
suspend fun findMangaById(mangaId: Long): Manga? { suspend fun findMangaById(mangaId: Long): Manga? {
return db.mangaDao().find(mangaId)?.toManga() return db.mangaDao.find(mangaId)?.toManga()
} }
suspend fun storeManga(manga: Manga) { suspend fun storeManga(manga: Manga) {
db.mangaDao().upsert(MangaEntity.from(manga), manga.tags.map(TagEntity.Companion::fromMangaTag)) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
}
} }
} }

View File

@@ -9,15 +9,23 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory : KoinComponent { object MangaProviderFactory : KoinComponent {
val sources: List<MangaSource> fun getSources(includeHidden: Boolean): List<MangaSource> {
get() { val settings = get<AppSettings>()
val list = MangaSource.values().toList() - MangaSource.LOCAL val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = get<AppSettings>().sourcesOrder val order = settings.sourcesOrder
return list.sortedBy { x -> val hidden = settings.hiddenSources
val e = order.indexOf(x.ordinal) val sorted = list.sortedBy { x ->
if (e == -1) order.size + x.ordinal else e val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if(includeHidden) {
sorted
} else {
sorted.filterNot { x ->
x.name in hidden
} }
} }
}
fun createLocal() = LocalMangaRepository() fun createLocal() = LocalMangaRepository()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain.favourites package org.koitharu.kotatsu.domain.favourites
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@@ -18,17 +19,17 @@ class FavouritesRepository : KoinComponent {
private val db: MangaDatabase by inject() private val db: MangaDatabase by inject()
suspend fun getAllManga(offset: Int): List<Manga> { suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao().findAll(offset, 20, "created_at") val entities = db.favouritesDao.findAll(offset, 20, "created_at")
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun getAllCategories(): List<FavouriteCategory> { suspend fun getAllCategories(): List<FavouriteCategory> {
val entities = db.favouriteCategoriesDao().findAll("created_at") val entities = db.favouriteCategoriesDao.findAll("created_at")
return entities.map { it.toFavouriteCategory() } return entities.map { it.toFavouriteCategory() }
} }
suspend fun getCategories(mangaId: Long): List<FavouriteCategory> { suspend fun getCategories(mangaId: Long): List<FavouriteCategory> {
val entities = db.favouritesDao().find(mangaId)?.categories val entities = db.favouritesDao.find(mangaId)?.categories
return entities?.map { it.toFavouriteCategory() }.orEmpty() return entities?.map { it.toFavouriteCategory() }.orEmpty()
} }
@@ -38,25 +39,30 @@ class FavouritesRepository : KoinComponent {
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
categoryId = 0 categoryId = 0
) )
val id = db.favouriteCategoriesDao().insert(entity) val id = db.favouriteCategoriesDao.insert(entity)
return entity.toFavouriteCategory(id) return entity.toFavouriteCategory(id)
} }
suspend fun renameCategory(id: Long, title: String) {
db.favouriteCategoriesDao.update(id, title)
}
suspend fun removeCategory(id: Long) { suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao().delete(id) db.favouriteCategoriesDao.delete(id)
} }
suspend fun addToCategory(manga: Manga, categoryId: Long) { suspend fun addToCategory(manga: Manga, categoryId: Long) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.tagsDao().upsert(tags) db.withTransaction {
db.mangaDao().upsert(MangaEntity.from(manga), tags) db.tagsDao.upsert(tags)
val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis()) db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.favouritesDao().add(entity) val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis())
db.favouritesDao.add(entity)
}
notifyFavouritesChanged(manga.id) notifyFavouritesChanged(manga.id)
} }
suspend fun removeFromCategory(manga: Manga, categoryId: Long) { suspend fun removeFromCategory(manga: Manga, categoryId: Long) {
db.favouritesDao().delete(categoryId, manga.id) db.favouritesDao.delete(categoryId, manga.id)
notifyFavouritesChanged(manga.id) notifyFavouritesChanged(manga.id)
} }

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain.history package org.koitharu.kotatsu.domain.history
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import java.util.* import java.util.*
class HistoryRepository : KoinComponent { class HistoryRepository : KoinComponent {
@@ -17,47 +19,65 @@ class HistoryRepository : KoinComponent {
private val db: MangaDatabase by inject() private val db: MangaDatabase by inject()
suspend fun getList(offset: Int, limit: Int = 20): List<Manga> { suspend fun getList(offset: Int, limit: Int = 20): List<Manga> {
val entities = db.historyDao().findAll(offset, limit) val entities = db.historyDao.findAll(offset, limit)
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Float) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.tagsDao().upsert(tags) db.withTransaction {
db.mangaDao().upsert(MangaEntity.from(manga), tags) db.tagsDao.upsert(tags)
db.historyDao().upsert( db.mangaDao.upsert(MangaEntity.from(manga), tags)
HistoryEntity( if (db.historyDao.upsert(
mangaId = manga.id, HistoryEntity(
createdAt = System.currentTimeMillis(), mangaId = manga.id,
updatedAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
chapterId = chapterId, updatedAt = System.currentTimeMillis(),
page = page chapterId = chapterId,
) page = page,
) scroll = scroll
)
)
) {
TrackingRepository().insertOrNothing(manga)
}
}
notifyHistoryChanged() notifyHistoryChanged()
} }
suspend fun getOne(manga: Manga): MangaHistory? { suspend fun getOne(manga: Manga): MangaHistory? {
return db.historyDao().find(manga.id)?.let { return db.historyDao.find(manga.id)?.let {
MangaHistory( MangaHistory(
createdAt = Date(it.createdAt), createdAt = Date(it.createdAt),
updatedAt = Date(it.updatedAt), updatedAt = Date(it.updatedAt),
chapterId = it.chapterId, chapterId = it.chapterId,
page = it.page page = it.page,
scroll = it.scroll
) )
} }
} }
suspend fun clear() { suspend fun clear() {
db.historyDao().clear() db.historyDao.clear()
notifyHistoryChanged() notifyHistoryChanged()
} }
suspend fun delete(manga: Manga) { suspend fun delete(manga: Manga) {
db.historyDao().delete(manga.id) db.historyDao.delete(manga.id)
notifyHistoryChanged() notifyHistoryChanged()
} }
/**
* Try to replace one manga with another one
* Useful for replacing saved manga on deleting it with remove source
*/
suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) {
if (alternative == null || db.mangaDao.update(MangaEntity.from(alternative)) <= 0) {
db.historyDao.delete(manga.id)
notifyHistoryChanged()
}
}
companion object { companion object {
private val listeners = HashSet<OnHistoryChangeListener>() private val listeners = HashSet<OnHistoryChangeListener>()

View File

@@ -14,7 +14,7 @@ class MangaIndex(source: String?) {
private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject() private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject()
fun setMangaInfo(manga: Manga) { fun setMangaInfo(manga: Manga, append: Boolean) {
json.put("id", manga.id) json.put("id", manga.id)
json.put("title", manga.title) json.put("title", manga.title)
json.put("title_alt", manga.altTitle) json.put("title_alt", manga.altTitle)
@@ -32,7 +32,9 @@ class MangaIndex(source: String?) {
a.put(jo) a.put(jo)
} }
}) })
json.put("chapters", JSONObject()) if (!append || !json.has("chapters")) {
json.put("chapters", JSONObject())
}
json.put("app_id", BuildConfig.APPLICATION_ID) json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE) json.put("app_version", BuildConfig.VERSION_CODE)
} }

View File

@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.takeIfReadable
import org.koitharu.kotatsu.utils.ext.toFileName import org.koitharu.kotatsu.utils.ext.toFileNameSafe
import java.io.File import java.io.File
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -17,11 +17,12 @@ class MangaZip(val file: File) {
private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() } private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() }
?: throw RuntimeException("Cannot create temporary directory") ?: throw RuntimeException("Cannot create temporary directory")
private val index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) private var index = MangaIndex(null)
fun prepare(manga: Manga) { fun prepare(manga: Manga) {
extract() extract()
index.setMangaInfo(manga) index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText())
index.setMangaInfo(manga, append = true)
} }
fun cleanup() { fun cleanup() {
@@ -90,7 +91,7 @@ class MangaZip(val file: File) {
const val INDEX_ENTRY = "index.json" const val INDEX_ENTRY = "index.json"
fun findInDir(root: File, manga: Manga): MangaZip { fun findInDir(root: File, manga: Manga): MangaZip {
val name = manga.title.toFileName() + ".cbz" val name = manga.title.toFileNameSafe() + ".cbz"
val file = File(root, name) val file = File(root, name)
return MangaZip(file) return MangaZip(file)
} }

View File

@@ -0,0 +1,67 @@
package org.koitharu.kotatsu.domain.tracking
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.TrackEntity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaTracking
import java.util.*
class TrackingRepository : KoinComponent {
private val db: MangaDatabase by inject()
suspend fun getNewChaptersCount(mangaId: Long): Int {
val entity = db.tracksDao.find(mangaId) ?: return 0
return entity.newChapters
}
suspend fun getAllTracks(): List<MangaTracking> {
val favourites = db.favouritesDao.findAllManga()
val history = db.historyDao.findAllManga()
val manga = (favourites + history).distinctBy { it.id }
val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
return manga.map { m ->
val track = tracks[m.id]?.singleOrNull()
MangaTracking(
manga = m.toManga(),
knownChaptersCount = track?.totalChapters ?: -1,
lastChapterId = track?.lastChapterId ?: 0L,
lastNotifiedChapterId = track?.lastNotifiedChapterId ?: 0L,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
)
}
}
suspend fun storeTrackResult(
mangaId: Long,
knownChaptersCount: Int,
lastChapterId: Long,
newChapters: Int,
lastNotifiedChapterId: Long
) {
val entity = TrackEntity(
mangaId = mangaId,
newChapters = newChapters,
lastCheck = System.currentTimeMillis(),
lastChapterId = lastChapterId,
totalChapters = knownChaptersCount,
lastNotifiedChapterId = lastNotifiedChapterId
)
db.tracksDao.upsert(entity)
}
suspend fun insertOrNothing(manga: Manga) {
val chapters = manga.chapters ?: return
val entity = TrackEntity(
mangaId = manga.id,
totalChapters = chapters.size,
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
newChapters = 0,
lastCheck = System.currentTimeMillis(),
lastNotifiedChapterId = 0L
)
db.tracksDao.insert(entity)
}
}

View File

@@ -0,0 +1,94 @@
package org.koitharu.kotatsu.ui.browser
import android.annotation.SuppressLint
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 androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_browser.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity(), BrowserCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_browser)
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_cross)
}
with(webView.settings) {
javaScriptEnabled = true
}
webView.webViewClient = BrowserClient(this)
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finish()
} else {
webView.loadUrl(url)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
webView.stopLoading()
finish()
true
}
R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(webView.url)
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
webView.onResume()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
this.title = title
supportActionBar?.subtitle = subtitle
}
companion object {
@JvmStatic
fun newIntent(context: Context, url: String) = Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
}
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.ui.browser
interface BrowserCallback {
fun onLoadingStateChanged(isLoading: Boolean)
fun onTitleChanged(title: CharSequence, subtitle: CharSequence?)
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.ui.browser
import android.graphics.Bitmap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.utils.ext.safe
class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), KoinComponent {
private val okHttp by inject<OkHttpClient>()
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
callback.onLoadingStateChanged(isLoading = false)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
callback.onLoadingStateChanged(isLoading = true)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title, url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false
override fun shouldOverrideUrlLoading(view: WebView, url: String) = false
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
return url?.let(::doRequest)
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
return request?.url?.toString()?.let(::doRequest)
}
private fun doRequest(url: String): WebResourceResponse? = safe {
val request = Request.Builder()
.url(url)
.build()
val response = okHttp.newCall(request).execute()
val ct = response.body?.contentType()
WebResourceResponse(
"${ct?.type}/${ct?.subtype}",
ct?.charset()?.name() ?: "utf-8",
response.body?.byteStream()
)
}
}

View File

@@ -11,7 +11,6 @@ import moxy.MvpAppCompatActivity
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.dialog.StorageSelectDialog
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
@@ -70,7 +69,7 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
return true return true
} }
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
StorageSelectDialog.Builder(this).create().show() throw StackOverflowError("test")
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)

View File

@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.ui.common
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import org.koin.core.KoinComponent import org.koin.android.ext.android.inject
import org.koin.core.inject
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), KoinComponent { PreferenceFragmentCompat() {
protected val settings by inject<AppSettings>() protected val settings by inject<AppSettings>()

View File

@@ -7,10 +7,9 @@ import androidx.annotation.CallSuper
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.koin.core.KoinComponent
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
abstract class BaseService : Service(), KoinComponent, CoroutineScope { abstract class BaseService : Service(), CoroutineScope {
private val job = SupervisorJob() private val job = SupervisorJob()

View File

@@ -3,28 +3,16 @@ package org.koitharu.kotatsu.ui.common.dialog
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_input.view.* import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.showKeyboard
class TextInputDialog private constructor(private val delegate: AlertDialog) : class TextInputDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate { DialogInterface by delegate {
init {
delegate.setOnShowListener {
val view = delegate.findViewById<TextView>(R.id.inputEdit)?:return@setOnShowListener
view.post {
view.requestFocus()
view.showKeyboard()
}
}
}
fun show() = delegate.show() fun show() = delegate.show()
class Builder(context: Context) { class Builder(context: Context) {
@@ -34,10 +22,6 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
private val delegate = AlertDialog.Builder(context) private val delegate = AlertDialog.Builder(context)
.setView(view) .setView(view)
.setOnDismissListener {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
}
fun setTitle(@StringRes titleResId: Int): Builder { fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId) delegate.setTitle(titleResId)
@@ -54,11 +38,28 @@ class TextInputDialog private constructor(private val delegate: AlertDialog) :
return this return this
} }
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
with(view.inputLayout) {
counterMaxLength = maxLength
isCounterEnabled = maxLength > 0
}
if (strict && maxLength > 0) {
view.inputEdit.filters += InputFilter.LengthFilter(maxLength)
}
return this
}
fun setInputType(inputType: Int): Builder { fun setInputType(inputType: Int): Builder {
view.inputEdit.inputType = inputType view.inputEdit.inputType = inputType
return this return this
} }
fun setText(text: String): Builder {
view.inputEdit.setText(text)
view.inputEdit.setSelection(text.length)
return this
}
fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder { fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder {
delegate.setPositiveButton(textId) { dialog, _ -> delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, view.inputEdit.text?.toString().orEmpty()) listener(dialog, view.inputEdit.text?.toString().orEmpty())

View File

@@ -80,6 +80,10 @@ abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecy
onDataSetChanged() onDataSetChanged()
} }
override fun onViewRecycled(holder: BaseViewHolder<T, E>) {
holder.onRecycled()
}
final override fun getItemCount() = dataSet.size final override fun getItemCount() = dataSet.size
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> { final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.ui.common.list package org.koitharu.kotatsu.ui.common.list
import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
@@ -31,14 +32,21 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T, E> { fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?): BaseViewHolder<T, E> {
if (listener != null) { if (listener != null) {
itemView.setOnClickListener { itemView.setOnClickListener {
listener.onItemClick(boundData ?: return@setOnClickListener, adapterPosition, it) listener.onItemClick(boundData ?: return@setOnClickListener, bindingAdapterPosition, it)
} }
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
listener.onItemLongClick(boundData ?: return@setOnLongClickListener false, adapterPosition, it) listener.onItemLongClick(boundData ?: return@setOnLongClickListener false, bindingAdapterPosition, it)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemView.setOnContextClickListener {
listener.onItemLongClick(boundData ?: return@setOnContextClickListener false, bindingAdapterPosition, it)
}
} }
} }
return this return this
} }
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E) abstract fun onBind(data: T, extra: E)
} }

View File

@@ -0,0 +1,67 @@
package org.koitharu.kotatsu.ui.common.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.Checkable
import androidx.appcompat.widget.AppCompatImageView
class CheckableImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr), Checkable {
private var isCheckedInternal = false
private var isBroadcasting = false
var onCheckedChangeListener: OnCheckedChangeListener? = null
init {
setOnClickListener {
toggle()
}
}
fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) {
onCheckedChangeListener = object : OnCheckedChangeListener {
override fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean) {
listener(isChecked)
}
}
}
override fun isChecked() = isCheckedInternal
override fun toggle() {
isChecked = !isCheckedInternal
}
override fun setChecked(checked: Boolean) {
if (checked != isCheckedInternal) {
isCheckedInternal = checked
refreshDrawableState()
if (!isBroadcasting) {
isBroadcasting = true
onCheckedChangeListener?.onCheckedChanged(this, checked)
isBroadcasting = false
}
}
}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val state = super.onCreateDrawableState(extraSpace + 1)
if (isCheckedInternal) {
mergeDrawableStates(state, CHECKED_STATE_SET)
}
return state
}
interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
}
private companion object {
@JvmStatic
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@@ -16,6 +16,13 @@ class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChap
updateCurrentPosition() updateCurrentPosition()
} }
var newChaptersCount: Int = 0
set(value) {
val updated = maxOf(field, value)
field = value
notifyItemRangeChanged(itemCount - updated, updated)
}
var currentChapterPosition = RecyclerView.NO_POSITION var currentChapterPosition = RecyclerView.NO_POSITION
private set private set
@@ -24,9 +31,13 @@ class ChaptersAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaChap
override fun onGetItemId(item: MangaChapter) = item.id override fun onGetItemId(item: MangaChapter) = item.id
override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when { override fun getExtra(item: MangaChapter, position: Int): ChapterExtra = when {
currentChapterPosition == RecyclerView.NO_POSITION -> ChapterExtra.UNREAD currentChapterPosition == RecyclerView.NO_POSITION
|| currentChapterPosition < position -> if (position >= itemCount - newChaptersCount) {
ChapterExtra.NEW
} else {
ChapterExtra.UNREAD
}
currentChapterPosition == position -> ChapterExtra.CURRENT currentChapterPosition == position -> ChapterExtra.CURRENT
currentChapterPosition < position -> ChapterExtra.UNREAD
currentChapterPosition > position -> ChapterExtra.READ currentChapterPosition > position -> ChapterExtra.READ
else -> ChapterExtra.UNREAD else -> ChapterExtra.UNREAD
} }

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import android.app.ActivityOptions
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_chapters.* import kotlinx.android.synthetic.main.fragment_chapters.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
@@ -44,6 +46,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
override fun onMangaUpdated(manga: Manga) { override fun onMangaUpdated(manga: Manga) {
this.manga = manga this.manga = manga
adapter.replaceData(manga.chapters.orEmpty()) adapter.replaceData(manga.chapters.orEmpty())
scrollToCurrent()
} }
override fun onLoadingStateChanged(isLoading: Boolean) { override fun onLoadingStateChanged(isLoading: Boolean) {
@@ -56,17 +59,29 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
override fun onHistoryChanged(history: MangaHistory?) { override fun onHistoryChanged(history: MangaHistory?) {
adapter.currentChapterId = history?.chapterId adapter.currentChapterId = history?.chapterId
scrollToCurrent()
}
override fun onNewChaptersChanged(newChapters: Int) {
adapter.newChaptersCount = newChapters
} }
override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit override fun onFavouriteChanged(categories: List<FavouriteCategory>) = Unit
override fun onItemClick(item: MangaChapter, position: Int, view: View) { override fun onItemClick(item: MangaChapter, position: Int, view: View) {
val options = ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.measuredWidth,
view.measuredHeight
)
startActivity( startActivity(
ReaderActivity.newIntent( ReaderActivity.newIntent(
context ?: return, context ?: return,
manga ?: return, manga ?: return,
item.id item.id
) ), options.toBundle()
) )
} }
@@ -86,4 +101,13 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
} }
return true return true
} }
private fun scrollToCurrent() {
val pos = (recyclerView_chapters.adapter as? ChaptersAdapter)?.currentChapterPosition
?: RecyclerView.NO_POSITION
if (pos != RecyclerView.NO_POSITION) {
(recyclerView_chapters.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(pos, 100)
}
}
} }

View File

@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -21,12 +22,12 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.browser.BrowserActivity
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.download.DownloadService import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ShortcutUtils
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showDialog
class MangaDetailsActivity : BaseActivity(), MangaDetailsView { class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
@@ -78,6 +79,17 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
} }
} }
override fun onNewChaptersChanged(newChapters: Int) {
val tab = tabs.getTabAt(1) ?: return
if (newChapters == 0) {
tab.removeBadge()
} else {
val badge = tab.orCreateBadge
badge.number = newChapters
badge.isVisible = true
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_details, menu) menuInflater.inflate(R.menu.opt_details, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
@@ -106,27 +118,44 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
} }
R.id.action_delete -> { R.id.action_delete -> {
manga?.let { m -> manga?.let { m ->
showDialog { AlertDialog.Builder(this)
setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
setMessage(getString(R.string.text_delete_local_manga, m.title)) .setMessage(getString(R.string.text_delete_local_manga, m.title))
setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
presenter.deleteLocal(m) presenter.deleteLocal(m)
} }
setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
} .show()
} }
true true
} }
R.id.action_save -> { R.id.action_save -> {
manga?.let { manga?.let {
DownloadService.start(this, it) val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) {
AlertDialog.Builder(this)
.setTitle(R.string.save_manga)
.setMessage(getString(R.string.large_manga_save_confirm, chaptersCount))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, it)
}.show()
} else {
DownloadService.start(this, it)
}
}
true
}
R.id.action_browser -> {
manga?.let {
startActivity(BrowserActivity.newIntent(this, it.url))
} }
true true
} }
R.id.action_shortcut -> { R.id.action_shortcut -> {
manga?.let { manga?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!ShortcutUtils.requestPinShortcut(this@MangaDetailsActivity, manga)) { if (!MangaShortcut(it).requestPinShortcut(this@MangaDetailsActivity)) {
Snackbar.make( Snackbar.make(
pager, pager,
R.string.operation_not_supported, R.string.operation_not_supported,
@@ -143,7 +172,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
companion object { companion object {
private const val EXTRA_MANGA = "manga" private const val EXTRA_MANGA = "manga"
private const val EXTRA_MANGA_ID = "manga_id" const val EXTRA_MANGA_ID = "manga_id"
const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA" const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA"

View File

@@ -13,14 +13,17 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesDialog import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.search.MangaSearchSheet import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.utils.ext.addChips import org.koitharu.kotatsu.utils.ext.addChips
import org.koitharu.kotatsu.utils.ext.showPopupMenu
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView, View.OnClickListener { class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView,
View.OnClickListener,
View.OnLongClickListener {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance)
@@ -64,9 +67,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
onClickListener = this@MangaDetailsFragment onClickListener = this@MangaDetailsFragment
) )
} }
imageView_favourite.setOnClickListener { imageView_favourite.setOnClickListener(this)
FavouriteCategoriesDialog.show(childFragmentManager, manga) button_read.setOnClickListener(this)
} button_read.setOnLongClickListener(this)
updateReadButton() updateReadButton()
} }
@@ -93,12 +96,55 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
override fun onMangaRemoved(manga: Manga) = Unit //handled in activity override fun onMangaRemoved(manga: Manga) = Unit //handled in activity
override fun onNewChaptersChanged(newChapters: Int) = Unit
override fun onClick(v: View) { override fun onClick(v: View) {
if (v is Chip) { when {
when(val tag = v.tag) { v.id == R.id.imageView_favourite -> {
is String -> MangaSearchSheet.show(activity?.supportFragmentManager ?: childFragmentManager, FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
manga?.source ?: return, tag)
} }
v.id == R.id.button_read -> {
startActivity(
ReaderActivity.newIntent(
context ?: return,
manga ?: return,
history
)
)
}
v is Chip -> {
when (val tag = v.tag) {
is String -> MangaSearchSheet.show(activity?.supportFragmentManager
?: childFragmentManager,
manga?.source ?: return, tag)
}
}
}
}
override fun onLongClick(v: View): Boolean {
when {
v.id == R.id.button_read -> {
if (history == null) {
return false
}
v.showPopupMenu(R.menu.popup_read) {
when (it.itemId) {
R.id.action_read -> {
startActivity(
ReaderActivity.newIntent(
context ?: return@showPopupMenu false,
manga ?: return@showPopupMenu false
)
)
true
}
else -> false
}
}
return true
}
else -> return false
} }
} }
@@ -114,15 +160,6 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
button_read.setText(R.string._continue) button_read.setText(R.string._continue)
button_read.setIconResource(R.drawable.ic_play) button_read.setIconResource(R.drawable.ic_play)
} }
button_read.setOnClickListener {
startActivity(
ReaderActivity.newIntent(
context ?: return@setOnClickListener,
manga ?: return@setOnClickListener,
history
)
)
}
} }
} }
} }

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException import java.io.IOException
@@ -28,12 +29,14 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
private lateinit var historyRepository: HistoryRepository private lateinit var historyRepository: HistoryRepository
private lateinit var favouritesRepository: FavouritesRepository private lateinit var favouritesRepository: FavouritesRepository
private lateinit var trackingRepository: TrackingRepository
private var manga: Manga? = null private var manga: Manga? = null
override fun onFirstViewAttach() { override fun onFirstViewAttach() {
historyRepository = HistoryRepository() historyRepository = HistoryRepository()
favouritesRepository = FavouritesRepository() favouritesRepository = FavouritesRepository()
trackingRepository = TrackingRepository()
super.onFirstViewAttach() super.onFirstViewAttach()
HistoryRepository.subscribe(this) HistoryRepository.subscribe(this)
FavouritesRepository.subscribe(this) FavouritesRepository.subscribe(this)
@@ -75,6 +78,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} }
viewState.onMangaUpdated(data) viewState.onMangaUpdated(data)
this@MangaDetailsPresenter.manga = data this@MangaDetailsPresenter.manga = data
viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id))
} catch (_: CancellationException){ } catch (_: CancellationException){
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@@ -94,9 +98,10 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val repository = val repository =
MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file") repository.delete(manga) || throw IOException("Unable to delete file")
safe { safe {
HistoryRepository().delete(manga) HistoryRepository().deleteOrSwap(manga, original)
} }
} }
viewState.onMangaRemoved(manga) viewState.onMangaRemoved(manga)

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.ui.details package org.koitharu.kotatsu.ui.details
import moxy.MvpView
import moxy.viewstate.strategy.alias.AddToEndSingle import moxy.viewstate.strategy.alias.AddToEndSingle
import moxy.viewstate.strategy.alias.OneExecution
import moxy.viewstate.strategy.alias.SingleState import moxy.viewstate.strategy.alias.SingleState
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -22,4 +20,7 @@ interface MangaDetailsView : BaseMvpView {
@SingleState @SingleState
fun onMangaRemoved(manga: Manga) fun onMangaRemoved(manga: Manga)
@AddToEndSingle
fun onNewChaptersChanged(newChapters: Int)
} }

View File

@@ -5,10 +5,11 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
@@ -31,9 +32,13 @@ class DownloadNotification(private val context: Context) {
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
channel.enableVibration(false) channel.enableVibration(false)
channel.enableLights(false)
channel.setSound(null, null)
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
} }
builder.setOnlyAlertOnce(true) builder.setOnlyAlertOnce(true)
builder.setDefaults(0)
builder.color = ContextCompat.getColor(context, R.color.blue_primary)
} }
fun fillFrom(manga: Manga) { fun fillFrom(manga: Manga) {
@@ -70,10 +75,11 @@ class DownloadNotification(private val context: Context) {
builder.setContentText(e.getDisplayMessage(context.resources)) builder.setContentText(e.getDisplayMessage(context.resources))
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setContentIntent(null) builder.setContentIntent(null)
builder.setCategory(NotificationCompat.CATEGORY_ERROR)
} }
fun setLargeIcon(icon: Drawable?) { fun setLargeIcon(icon: Drawable?) {
builder.setLargeIcon((icon as? BitmapDrawable)?.bitmap) builder.setLargeIcon(icon?.toBitmap())
} }
fun setProgress(chaptersTotal: Int, pagesTotal: Int, chapter: Int, page: Int) { fun setProgress(chaptersTotal: Int, pagesTotal: Int, chapter: Int, page: Int) {
@@ -83,6 +89,7 @@ class DownloadNotification(private val context: Context) {
val percent = (progress / max.toFloat() * 100).roundToInt() val percent = (progress / max.toFloat() * 100).roundToInt()
builder.setProgress(max, progress, false) builder.setProgress(max, progress, false)
builder.setContentText("%d%%".format(percent)) builder.setContentText("%d%%".format(percent))
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
} }
fun setPostProcessing() { fun setPostProcessing() {
@@ -96,6 +103,7 @@ class DownloadNotification(private val context: Context) {
builder.setContentIntent(createIntent(context, manga)) builder.setContentIntent(createIntent(context, manga))
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setSmallIcon(android.R.drawable.stat_sys_download_done) builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
builder.setCategory(null)
} }
fun setCancelling() { fun setCancelling() {

View File

@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.ui.download
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.PowerManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.api.get import coil.api.get
@@ -11,7 +13,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.local.PagesCache
@@ -27,11 +29,13 @@ import org.koitharu.kotatsu.utils.ext.retryUntilSuccess
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.sub
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class DownloadService : BaseService() { class DownloadService : BaseService() {
private lateinit var notification: DownloadNotification private lateinit var notification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
@@ -41,6 +45,8 @@ class DownloadService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notification = DownloadNotification(this) notification = DownloadNotification(this)
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -50,6 +56,7 @@ class DownloadService : BaseService() {
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet()
if (manga != null) { if (manga != null) {
jobs[startId] = downloadManga(manga, chapters, startId) jobs[startId] = downloadManga(manga, chapters, startId)
Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
} else { } else {
stopSelf(startId) stopSelf(startId)
} }
@@ -67,6 +74,7 @@ class DownloadService : BaseService() {
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job { private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
return launch(Dispatchers.IO) { return launch(Dispatchers.IO) {
mutex.lock() mutex.lock()
wakeLock.acquire(TimeUnit.MINUTES.toMillis(20))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.fillFrom(manga) notification.fillFrom(manga)
notification.setCancelId(startId) notification.setCancelId(startId)
@@ -154,6 +162,7 @@ class DownloadService : BaseService() {
notification.dismiss() notification.dismiss()
stopSelf(startId) stopSelf(startId)
} }
wakeLock.release()
mutex.unlock() mutex.unlock()
} }
} }

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.main
import android.app.ActivityOptions
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -28,8 +30,9 @@ import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.settings.AppUpdateService import org.koitharu.kotatsu.ui.settings.AppUpdateService
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
@@ -44,7 +47,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
drawerToggle = drawerToggle =
ActionBarDrawerToggle(this, drawer, toolbar, R.string.open_menu, R.string.close_menu) ActionBarDrawerToggle(this, drawer, toolbar, R.string.open_menu, R.string.close_menu)
drawer.addDrawerListener(drawerToggle) drawer.addDrawerListener(drawerToggle)
@@ -66,9 +68,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setCheckedItem(R.id.nav_history) navigationView.setCheckedItem(R.id.nav_history)
setPrimaryFragment(HistoryListFragment.newInstance()) setPrimaryFragment(HistoryListFragment.newInstance())
} }
drawer.postDelayed(4000) { drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext) AppUpdateService.startIfRequired(applicationContext)
} }
TrackWorker.setup(applicationContext)
} }
override fun onDestroy() { override fun onDestroy() {
@@ -79,7 +82,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
drawerToggle.syncState() drawerToggle.syncState()
initSideMenu(MangaProviderFactory.sources) initSideMenu(MangaProviderFactory.getSources(includeHidden = false))
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@@ -117,7 +120,16 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
override fun onOpenReader(state: ReaderState) { override fun onOpenReader(state: ReaderState) {
startActivity(ReaderActivity.newIntent(this, state)) val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ActivityOptions.makeClipRevealAnimation(
fab, 0, 0, fab.measuredWidth, fab.measuredHeight
)
} else {
ActivityOptions.makeScaleUpAnimation(
fab, 0, 0, fab.measuredWidth, fab.measuredHeight
)
}
startActivity(ReaderActivity.newIntent(this, state), options?.toBundle())
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
@@ -148,7 +160,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
getString(R.string.key_sources_order) -> initSideMenu(MangaProviderFactory.sources) getString(R.string.key_sources_hidden),
getString(R.string.key_sources_order) -> {
initSideMenu(MangaProviderFactory.getSources(includeHidden = false))
}
} }
} }

View File

@@ -26,7 +26,7 @@ class MainPresenter : BasePresenter<MainView>() {
val history = repo.getOne(manga) ?: throw EmptyHistoryException() val history = repo.getOne(manga) ?: throw EmptyHistoryException()
ReaderState( ReaderState(
MangaProviderFactory.create(manga.source).getDetails(manga), MangaProviderFactory.create(manga.source).getDetails(manga),
history.chapterId, history.page history.chapterId, history.page, history.scroll
) )
} }
viewState.onOpenReader(state) viewState.onOpenReader(state)

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.main.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear
import coil.api.load import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_grid.* import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -11,15 +11,17 @@ import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_grid) { class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_grid) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga, extra: MangaHistory?) { override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose() imageView_cover.clear()
textView_title.text = data.title textView_title.text = data.title
coverRequest = imageView_cover.load(data.coverUrl) { imageView_cover.load(data.coverUrl) {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder)
} }
} }
override fun onRecycled() {
imageView_cover.clear()
}
} }

View File

@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.ui.main.list
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.api.clear
import coil.api.load import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list_details.* import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -15,14 +15,12 @@ import kotlin.math.roundToInt
class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list_details) { class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list_details) {
private var coverRequest: RequestDisposable? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBind(data: Manga, extra: MangaHistory?) { override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose() imageView_cover.clear()
textView_title.text = data.title textView_title.text = data.title
textView_subtitle.textAndVisible = data.altTitle textView_subtitle.textAndVisible = data.altTitle
coverRequest = imageView_cover.load(data.coverUrl) { imageView_cover.load(data.coverUrl) {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder)
@@ -37,4 +35,8 @@ class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga, MangaHis
it.title it.title
} }
} }
override fun onRecycled() {
imageView_cover.clear()
}
} }

View File

@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.MvpDelegate import moxy.MvpDelegate
@@ -39,7 +40,7 @@ import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>, abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>, PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener, SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback { SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
@@ -58,9 +59,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
initListMode(settings.listMode) initListMode(settings.listMode)
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener(this)
onRequestMoreItems(0)
}
recyclerView_filter.setHasFixedSize(true) recyclerView_filter.setHasFixedSize(true)
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this)) recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
@@ -122,6 +121,10 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
} }
} }
final override fun onRefresh() {
onRequestMoreItems(0)
}
override fun onListChanged(list: List<Manga>) { override fun onListChanged(list: List<Manga>) {
adapter?.replaceData(list) adapter?.replaceData(list)
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -171,6 +174,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show() Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
} }
@CallSuper
override fun onLoadingStateChanged(isLoading: Boolean) { override fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems progressBar.isVisible = isLoading && !hasItems
@@ -181,7 +185,11 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
} }
} }
@CallSuper
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (context == null) {
return
}
when (key) { when (key) {
getString(R.string.key_list_mode) -> initListMode(settings.listMode) getString(R.string.key_list_mode) -> initListMode(settings.listMode)
getString(R.string.key_grid_size) -> UiUtils.SpanCountResolver.update(recyclerView) getString(R.string.key_grid_size) -> UiUtils.SpanCountResolver.update(recyclerView)
@@ -229,6 +237,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)) ListMode.GRID -> GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx))
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.recycledViewPool.clear()
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
@@ -246,13 +255,13 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list), Mang
recyclerView.firstItem = position recyclerView.firstItem = position
} }
override fun isSection(position: Int): Boolean { final override fun isSection(position: Int): Boolean {
return position == 0 || recyclerView_filter.adapter?.run { return position == 0 || recyclerView_filter.adapter?.run {
getItemViewType(position) != getItemViewType(position - 1) getItemViewType(position) != getItemViewType(position - 1)
} ?: false } ?: false
} }
override fun getSectionTitle(position: Int): CharSequence? { final override fun getSectionTitle(position: Int): CharSequence? {
return when (recyclerView_filter.adapter?.getItemViewType(position)) { return when (recyclerView_filter.adapter?.getItemViewType(position)) {
FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order) FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order)
FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genre) FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genre)

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.main.list package org.koitharu.kotatsu.ui.main.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear
import coil.api.load import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list.* import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -13,16 +13,18 @@ import org.koitharu.kotatsu.utils.ext.textAndVisible
class MangaListHolder(parent: ViewGroup) : class MangaListHolder(parent: ViewGroup) :
BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list) { BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga, extra: MangaHistory?) { override fun onBind(data: Manga, extra: MangaHistory?) {
coverRequest?.dispose() imageView_cover.clear()
textView_title.text = data.title textView_title.text = data.title
textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title } textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title }
coverRequest = imageView_cover.load(data.coverUrl) { imageView_cover.load(data.coverUrl) {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder)
} }
} }
override fun onRecycled() {
imageView_cover.clear()
}
} }

View File

@@ -6,11 +6,11 @@ import android.view.MenuItem
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.ui.main.list.favourites.categories.CategoriesActivity
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{ class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> {
private val presenter by moxyPresenter(factory = ::FavouritesListPresenter) private val presenter by moxyPresenter(factory = ::FavouritesListPresenter)
@@ -19,12 +19,17 @@ class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// inflater.inflate(R.menu.opt_history, menu) inflater.inflate(R.menu.opt_favourites, menu)
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
} }
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_categories -> {
context?.let {
startActivity(CategoriesActivity.newIntent(it))
}
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }

View File

@@ -0,0 +1,113 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.text.InputType
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>,
FavouriteCategoriesView, View.OnClickListener {
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private lateinit var adapter: CategoriesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_categories)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
fab_add.imageTintList = ColorStateList.valueOf(Color.WHITE)
adapter = CategoriesAdapter(this)
recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
recyclerView.adapter = adapter
fab_add.setOnClickListener(this)
}
override fun onClick(v: View) {
when (v.id) {
R.id.fab_add -> createCategory()
}
}
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) {
view.showPopupMenu(R.menu.popup_category) {
when (it.itemId) {
R.id.action_remove -> deleteCategory(item)
R.id.action_rename -> renameCategory(item)
}
true
}
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories)
textView_holder.isVisible = categories.isEmpty()
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
override fun onError(e: Throwable) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG)
.show()
}
private fun deleteCategory(category: FavouriteCategory) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.remove) { _, _ ->
presenter.deleteCategory(category.id)
}.create()
.show()
}
private fun renameCategory(category: FavouriteCategory) {
TextInputDialog.Builder(this)
.setTitle(R.string.rename)
.setText(category.title)
.setHint(R.string.enter_category_name)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setMaxLength(12, false)
.setPositiveButton(R.string.rename) { _, name ->
presenter.renameCategory(category.id, name)
}.create()
.show()
}
private fun createCategory() {
TextInputDialog.Builder(this)
.setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setMaxLength(12, false)
.setPositiveButton(R.string.add) { _, name ->
presenter.createCategory(name)
}.create()
.show()
}
companion object {
fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java)
}
}

View File

@@ -1,46 +1,17 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.util.SparseBooleanArray
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Checkable
import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.CategoryCheckableHolder
class CategoriesAdapter(private val listener: OnCategoryCheckListener) : class CategoriesAdapter(onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>? = null) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() { BaseRecyclerAdapter<FavouriteCategory, Unit>(onItemClickListener) {
private val checkedIds = SparseBooleanArray() override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent)
fun setCheckedIds(ids: Iterable<Int>) {
checkedIds.clear()
ids.forEach {
checkedIds[it] = true
}
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) { override fun getExtra(item: FavouriteCategory, position: Int) = Unit
super.onViewHolderCreated(holder)
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
} }

View File

@@ -1,16 +1,15 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.* import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryHolder(parent: ViewGroup) : class CategoryHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) { BaseViewHolder<FavouriteCategory, Unit>(parent, R.layout.item_category) {
override fun onBind(data: FavouriteCategory, extra: Boolean) { override fun onBind(data: FavouriteCategory, extra: Unit) {
checkedTextView.text = data.title textView.text = data.title
checkedTextView.isChecked = extra
} }
} }

View File

@@ -70,6 +70,40 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
} }
} }
fun renameCategory(id: Long, name: String) {
presenterScope.launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.renameCategory(id, name)
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun deleteCategory(id: Long) {
presenterScope.launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.removeCategory(id)
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun addToCategory(manga: Manga, categoryId: Long) { fun addToCategory(manga: Manga, categoryId: Long) {
presenterScope.launch { presenterScope.launch {
try { try {

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = SparseBooleanArray()
fun setCheckedIds(ids: Iterable<Int>) {
checkedIds.clear()
ids.forEach {
checkedIds[it] = true
}
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryCheckableHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
super.onViewHolderCreated(holder)
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryCheckableHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
@@ -12,10 +12,12 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.common.BaseBottomSheet import org.koitharu.kotatsu.ui.common.BaseBottomSheet
import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesView
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_categories), class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
FavouriteCategoriesView, FavouriteCategoriesView,
OnCategoryCheckListener { OnCategoryCheckListener {
@@ -23,11 +25,13 @@ class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_cat
private val manga get() = arguments?.getParcelable<Manga>(ARG_MANGA) private val manga get() = arguments?.getParcelable<Manga>(ARG_MANGA)
private var adapter: CategoriesAdapter? = null private var adapter: CategoriesSelectAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = CategoriesAdapter(this) adapter =
CategoriesSelectAdapter(
this)
recyclerView_categories.adapter = adapter recyclerView_categories.adapter = adapter
textView_add.setOnClickListener { textView_add.setOnClickListener {
createCategory() createCategory()
@@ -66,6 +70,7 @@ class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_cat
TextInputDialog.Builder(context ?: return) TextInputDialog.Builder(context ?: return)
.setTitle(R.string.add_new_category) .setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name) .setHint(R.string.enter_category_name)
.setMaxLength(12, false)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) .setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel) .setNegativeButton(android.R.string.cancel)
.setPositiveButton(R.string.add) { _, name -> .setPositiveButton(R.string.add) { _, name ->
@@ -79,8 +84,10 @@ class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_cat
private const val ARG_MANGA = "manga" private const val ARG_MANGA = "manga"
private const val TAG = "FavouriteCategoriesDialog" private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog().withArgs(1) { fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
.withArgs(1) {
putParcelable(ARG_MANGA, manga) putParcelable(ARG_MANGA, manga)
}.show(fm, TAG) }.show(fm,
TAG)
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories.select
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory

View File

@@ -1,14 +1,9 @@
package org.koitharu.kotatsu.ui.main.list.history package org.koitharu.kotatsu.ui.main.list.history
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
@@ -53,7 +48,7 @@ class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<Man
} }
override fun setUpEmptyListHolder() { override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.history_is_empty) textView_holder.setText(R.string.text_history_holder)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
} }

View File

@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.utils.ShortcutUtils import org.koitharu.kotatsu.utils.MangaShortcut
@InjectViewState @InjectViewState
class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() { class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
@@ -62,7 +62,7 @@ class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
repository.clear() repository.clear()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutUtils.clearAppShortcuts(get()) MangaShortcut.clearAppShortcuts(get())
} }
viewState.onListChanged(emptyList()) viewState.onListChanged(emptyList())
} catch (_: CancellationException) { } catch (_: CancellationException) {
@@ -84,7 +84,7 @@ class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
repository.delete(manga) repository.delete(manga)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutUtils.removeAppShortcut(get(), manga) MangaShortcut(manga).removeAppShortcut(get())
} }
viewState.onItemRemoved(manga) viewState.onItemRemoved(manga)
} catch (_: CancellationException) { } catch (_: CancellationException) {

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
@@ -14,7 +15,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.ellipsize import org.koitharu.kotatsu.utils.ext.ellipsize
import org.koitharu.kotatsu.utils.ext.showDialog
import java.io.File import java.io.File
class LocalListFragment : MangaListFragment<File>() { class LocalListFragment : MangaListFragment<File>() {
@@ -59,7 +59,7 @@ class LocalListFragment : MangaListFragment<File>() {
} }
override fun setUpEmptyListHolder() { override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.no_saved_manga) textView_holder.setText(R.string.text_local_holder)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
} }
@@ -81,14 +81,14 @@ class LocalListFragment : MangaListFragment<File>() {
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean { override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
context?.showDialog { AlertDialog.Builder(context ?: return false)
setTitle(R.string.delete_manga) .setTitle(R.string.delete_manga)
setMessage(getString(R.string.text_delete_local_manga, data.title)) .setMessage(getString(R.string.text_delete_local_manga, data.title))
setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
presenter.delete(data) presenter.delete(data)
} }
setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
} .show()
true true
} }
else -> super.onPopupMenuItemSelected(item, data) else -> super.onPopupMenuItemSelected(item, data)

View File

@@ -2,12 +2,14 @@ package org.koitharu.kotatsu.ui.main.list.local
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.InjectViewState import moxy.InjectViewState
import moxy.presenterScope import moxy.presenterScope
import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -17,6 +19,7 @@ import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.domain.history.HistoryRepository import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.sub
@@ -88,11 +91,15 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
presenterScope.launch { presenterScope.launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file") repository.delete(manga) || throw IOException("Unable to delete file")
safe { safe {
HistoryRepository().delete(manga) HistoryRepository().deleteOrSwap(manga, original)
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
MangaShortcut(manga).removeAppShortcut(get())
}
viewState.onItemRemoved(manga) viewState.onItemRemoved(manga)
} catch (e: CancellationException) { } catch (e: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -16,21 +16,27 @@ import kotlin.coroutines.CoroutineContext
class PageLoader : KoinComponent, CoroutineScope, DisposableHandle { class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val job = SupervisorJob() private val job = SupervisorJob()
private val tasks = HashMap<String, Job>() private val tasks = HashMap<String, Deferred<File>>()
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun loadFile(url: String, force: Boolean): File { suspend fun loadFile(url: String, force: Boolean): File {
if (!force) { if (!force) {
cache[url]?.let { cache[url]?.let {
return it return it
} }
} }
val task = tasks[url]?.takeUnless { it.isCancelled }
return (task ?: loadAsync(url).also { tasks[url] = it }).await()
}
private fun loadAsync(url: String) = async(Dispatchers.IO) {
val uri = Uri.parse(url) val uri = Uri.parse(url)
return if (uri.scheme == "cbz") { if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart) val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use { zip.getInputStream(entry).use {
@@ -59,5 +65,6 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
override fun dispose() { override fun dispose() {
coroutineContext.cancel() coroutineContext.cancel()
tasks.clear()
} }
} }

View File

@@ -9,6 +9,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -34,8 +35,8 @@ import org.koitharu.kotatsu.ui.reader.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.ui.reader.wetoon.WebtoonReaderFragment import org.koitharu.kotatsu.ui.reader.wetoon.WebtoonReaderFragment
import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ShortcutUtils
import org.koitharu.kotatsu.utils.anim.Motion import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@@ -91,7 +92,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
GlobalScope.launch { GlobalScope.launch {
safe { safe {
ShortcutUtils.addAppShortcut(applicationContext, state.manga) MangaShortcut(state.manga).addAppShortcut(applicationContext)
} }
} }
} }
@@ -195,8 +196,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun saveState(chapterId: Long, page: Int) { override fun saveState(chapterId: Long, page: Int, scroll: Float) {
state = state.copy(chapterId = chapterId, page = page) state = state.copy(chapterId = chapterId, page = page, scroll = scroll)
ReaderPresenter.saveState(state) ReaderPresenter.saveState(state)
} }
@@ -207,16 +208,16 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
showDialog { val dialog = AlertDialog.Builder(this)
setTitle(R.string.error_occurred) .setTitle(R.string.error_occurred)
setMessage(e.message) .setMessage(e.message)
setPositiveButton(R.string.close, null) .setPositiveButton(R.string.close, null)
if (reader?.hasItems != true) { if (reader?.hasItems != true) {
setOnDismissListener { dialog.setOnDismissListener {
finish() finish()
}
} }
} }
dialog.show()
} }
override fun onGridTouch(area: Int) { override fun onGridTouch(area: Int) {
@@ -225,11 +226,13 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
setUiIsVisible(!appbar_top.isVisible) setUiIsVisible(!appbar_top.isVisible)
} }
GridTouchHelper.AREA_TOP, GridTouchHelper.AREA_TOP,
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_LEFT,
-> if (isTapSwitchEnabled) {
reader?.switchPageBy(-1) reader?.switchPageBy(-1)
} }
GridTouchHelper.AREA_BOTTOM, GridTouchHelper.AREA_BOTTOM,
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) { GridTouchHelper.AREA_RIGHT,
-> if (isTapSwitchEnabled) {
reader?.switchPageBy(1) reader?.switchPageBy(1)
} }
} }
@@ -267,13 +270,15 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT,
-> {
reader?.switchPageBy(1) reader?.switchPageBy(1)
true true
} }
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT,
-> {
reader?.switchPageBy(-1) reader?.switchPageBy(-1)
true true
} }
@@ -293,7 +298,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
override fun onChapterChanged(chapter: MangaChapter) { override fun onChapterChanged(chapter: MangaChapter) {
state = state.copy( state = state.copy(
chapterId = chapter.id, chapterId = chapter.id,
page = 0 page = 0,
scroll = 0f
) )
reader?.updateState(chapterId = chapter.id) reader?.updateState(chapterId = chapter.id)
} }
@@ -371,7 +377,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
manga = manga, manga = manga,
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
?: -1 else chapterId, ?: -1 else chapterId,
page = 0 page = 0,
scroll = 0f
) )
) )
@@ -383,7 +390,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
context, ReaderState( context, ReaderState(
manga = manga, manga = manga,
chapterId = history.chapterId, chapterId = history.chapterId,
page = history.page page = history.page,
scroll = history.scroll
) )
) )
} }

View File

@@ -7,5 +7,5 @@ interface ReaderListener : BaseMvpView {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
fun saveState(chapterId: Long, page: Int) fun saveState(chapterId: Long, page: Int, scroll: Float)
} }

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.ui.reader package org.koitharu.kotatsu.ui.reader
import android.content.ContentResolver import android.content.ContentResolver
import android.util.Log
import android.webkit.URLUtil import android.webkit.URLUtil
import kotlinx.coroutines.* import kotlinx.coroutines.*
import moxy.InjectViewState import moxy.InjectViewState
@@ -41,7 +40,7 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
mode = MangaUtils.determineReaderMode(pages) mode = MangaUtils.determineReaderMode(pages)
if (mode != null) { if (mode != null) {
prefs.savePreferences( prefs.savePreferences(
mangaId = manga.id, manga = manga,
mode = mode mode = mode
) )
} }
@@ -51,6 +50,9 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
viewState.onInitReader(manga, mode) viewState.onInitReader(manga, mode)
} catch (_: CancellationException) { } catch (_: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e) viewState.onError(e)
} finally { } finally {
viewState.onLoadingStateChanged(isLoading = false) viewState.onLoadingStateChanged(isLoading = false)
@@ -61,7 +63,7 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
fun setMode(manga: Manga, mode: ReaderMode) { fun setMode(manga: Manga, mode: ReaderMode) {
presenterScope.launch(Dispatchers.IO) { presenterScope.launch(Dispatchers.IO) {
MangaDataRepository().savePreferences( MangaDataRepository().savePreferences(
mangaId = manga.id, manga = manga,
mode = mode mode = mode
) )
} }
@@ -103,7 +105,8 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
HistoryRepository().addOrUpdate( HistoryRepository().addOrUpdate(
manga = state.manga, manga = state.manga,
chapterId = state.chapterId, chapterId = state.chapterId,
page = state.page page = state.page,
scroll = state.scroll
) )
} }
} }

View File

@@ -10,7 +10,8 @@ import org.koitharu.kotatsu.core.model.MangaChapter
data class ReaderState( data class ReaderState(
val manga: Manga, val manga: Manga,
val chapterId: Long, val chapterId: Long,
val page: Int val page: Int,
val scroll: Float
) : Parcelable { ) : Parcelable {
@IgnoredOnParcel @IgnoredOnParcel

View File

@@ -4,10 +4,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.commit import androidx.fragment.app.commit
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.settings.MainSettingsFragment
import org.koitharu.kotatsu.ui.settings.NetworkSettingsFragment
import org.koitharu.kotatsu.ui.settings.ReaderSettingsFragment import org.koitharu.kotatsu.ui.settings.ReaderSettingsFragment
import org.koitharu.kotatsu.ui.settings.SettingsHeadersFragment
class SimpleSettingsActivity : BaseActivity() { class SimpleSettingsActivity : BaseActivity() {
@@ -15,21 +17,20 @@ class SimpleSettingsActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings_simple) setContentView(R.layout.activity_settings_simple)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val section = intent?.getIntExtra(EXTRA_SECTION, 0)
supportFragmentManager.commit { supportFragmentManager.commit {
replace(R.id.container, when(section) { replace(R.id.container, when(intent?.action) {
SECTION_READER -> ReaderSettingsFragment() Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
else -> SettingsHeadersFragment() ACTION_READER -> ReaderSettingsFragment()
else -> MainSettingsFragment()
}) })
} }
} }
companion object { companion object {
private const val EXTRA_SECTION = "section" private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
private const val SECTION_READER = 1
fun newReaderSettingsIntent(context: Context) = Intent(context, SimpleSettingsActivity::class.java) fun newReaderSettingsIntent(context: Context) = Intent(context, SimpleSettingsActivity::class.java)
.putExtra(EXTRA_SECTION, SECTION_READER) .setAction(ACTION_READER)
} }
} }

View File

@@ -58,6 +58,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
pages.addLast(state.chapterId, it) pages.addLast(state.chapterId, it)
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
setCurrentItem(state.page, false) setCurrentItem(state.page, false)
if (state.scroll != 0f) {
restorePageScroll(state.page, state.scroll)
}
} }
} }
@@ -67,7 +70,8 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
ARG_STATE, ReaderState( ARG_STATE, ReaderState(
manga = manga, manga = manga,
chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return, chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return,
page = pages.getRelativeIndex(getCurrentItem()) page = pages.getRelativeIndex(getCurrentItem()),
scroll = getCurrentPageScroll()
) )
) )
} }
@@ -174,7 +178,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
val page = pages.getRelativeIndex(getCurrentItem()) val page = pages.getRelativeIndex(getCurrentItem())
if (page != -1) { if (page != -1) {
readerListener?.saveState(chapterId, page) readerListener?.saveState(chapterId, page, getCurrentPageScroll())
} }
Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)") Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)")
} }
@@ -217,6 +221,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
protected abstract fun getCurrentItem(): Int protected abstract fun getCurrentItem(): Int
protected abstract fun getCurrentPageScroll(): Float
protected abstract fun restorePageScroll(position: Int, scroll: Float)
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean) protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter

View File

@@ -55,7 +55,7 @@ class GroupedList<K, T> {
lruGroup = entry.second lruGroup = entry.second
lruGroupKey = entry.first lruGroupKey = entry.first
lruGroupFirstIndex = lastIndex - entry.second.size lruGroupFirstIndex = lastIndex - entry.second.size
return entry.second.get(index - lruGroupFirstIndex) return entry.second[index - lruGroupFirstIndex]
} }
lastIndex -= entry.second.size lastIndex -= entry.second.size
} }

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.ui.reader.standard
import android.view.View
import androidx.viewpager2.widget.ViewPager2
class PageAnimTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.apply {
val pageWidth = width
when {
position < -1 -> alpha = 0f
position <= 0 -> { // [-1,0]
alpha = 1f
translationX = 0f
translationZ = 0f
scaleX = 1 + FACTOR * position
scaleY = 1f
}
position <= 1 -> { // (0,1]
alpha = 1f
translationX = pageWidth * -position
translationZ = -1f
scaleX = 1f
scaleY = 1f
}
else -> alpha = 0f
}
}
}
private companion object {
const val FACTOR = 0.1f
}
}

View File

@@ -30,6 +30,11 @@ class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
doLoad(data, force = false) doLoad(data, force = false)
} }
override fun onRecycled() {
job?.cancel()
ssiv.recycle()
}
private fun doLoad(data: MangaPage, force: Boolean) { private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel() job?.cancel()
job = launch { job = launch {
@@ -49,7 +54,10 @@ class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
} }
} }
override fun onReady() = Unit override fun onReady() {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat())
ssiv.resetScaleAndCenter()
}
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageLoadError(e: Exception) = onError(e)

View File

@@ -1,30 +1,49 @@
package org.koitharu.kotatsu.ui.reader.standard package org.koitharu.kotatsu.ui.reader.standard
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.fragment_reader_standard.* import kotlinx.android.synthetic.main.fragment_reader_standard.*
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.GroupedList
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.utils.ext.doOnPageChanged import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard) { class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
SharedPreferences.OnSharedPreferenceChangeListener {
private var paginationListener: PagerPaginationListener? = null private var paginationListener: PagerPaginationListener? = null
private val settings by inject<AppSettings>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
paginationListener = PagerPaginationListener(adapter!!, 2, this) paginationListener = PagerPaginationListener(adapter!!, 2, this)
pager.adapter = adapter pager.adapter = adapter
if (settings.readerAnimation) {
pager.setPageTransformer(PageAnimTransformer())
}
pager.offscreenPageLimit = 2 pager.offscreenPageLimit = 2
pager.registerOnPageChangeCallback(paginationListener!!) pager.registerOnPageChangeCallback(paginationListener!!)
pager.doOnPageChanged(::notifyPageChanged) pager.doOnPageChanged(::notifyPageChanged)
} }
override fun onAttach(context: Context) {
super.onAttach(context)
settings.subscribe(this)
}
override fun onDetach() {
settings.unsubscribe(this)
super.onDetach()
}
override fun onDestroyView() { override fun onDestroyView() {
paginationListener = null paginationListener = null
super.onDestroyView() super.onDestroyView()
@@ -43,6 +62,22 @@ class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard)
pager.setCurrentItem(position, isSmooth) pager.setCurrentItem(position, isSmooth)
} }
override fun getCurrentPageScroll() = 0f
override fun restorePageScroll(position: Int, scroll: Float) = Unit
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
getString(R.string.key_reader_animation) -> {
if (settings.readerAnimation) {
pager.setPageTransformer(PageAnimTransformer())
} else {
pager.setPageTransformer(null)
}
}
}
}
companion object { companion object {
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) { fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.reader.thumbnails package org.koitharu.kotatsu.ui.reader.thumbnails
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import coil.Coil import coil.Coil
import coil.api.get import coil.api.get
import coil.size.PixelSize import coil.size.PixelSize
@@ -8,20 +9,18 @@ import coil.size.Size
import kotlinx.android.synthetic.main.item_page_thumb.* import kotlinx.android.synthetic.main.item_page_thumb.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope) : class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_thumb) { BaseViewHolder<MangaPage, PagesCache>(parent, R.layout.item_page_thumb) {
private var job: Job? = null private var job: Job? = null
private val thumbSize: Size private val thumbSize: Size
init { init {
// FIXME
// val color = DrawUtils.invertColor(textView_number.currentTextColor)
// textView_number.setShadowLayer(parent.resources.resolveDp(26f), 0f, 0f, color)
val width = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) val width = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
thumbSize = PixelSize( thumbSize = PixelSize(
width = width, width = width,
@@ -29,14 +28,15 @@ class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope)
) )
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: MangaPage, extra: PagesCache) {
imageView_thumb.setImageDrawable(null) imageView_thumb.setImageDrawable(null)
textView_number.text = (adapterPosition + 1).toString() textView_number.text = (bindingAdapterPosition + 1).toString()
job?.cancel() job?.cancel()
job = scope.launch(Dispatchers.IO) { job = scope.launch(Dispatchers.IO) {
try { try {
val url = data.preview ?: data.url.let { val url = data.preview ?: data.url.let {
MangaProviderFactory.create(data.source).getPageFullUrl(data) val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
extra[pageUrl]?.toUri()?.toString() ?: pageUrl
} }
val drawable = Coil.get(url) { val drawable = Coil.get(url) {
size(thumbSize) size(thumbSize)
@@ -50,4 +50,9 @@ class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope)
} }
} }
} }
override fun onRecycled() {
job?.cancel()
imageView_thumb.setImageDrawable(null)
}
} }

View File

@@ -5,15 +5,18 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.koin.core.inject
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class PagesThumbnailsAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaPage>?) : class PagesThumbnailsAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaPage>?) :
BaseRecyclerAdapter<MangaPage, Unit>(onItemClickListener), CoroutineScope, DisposableHandle { BaseRecyclerAdapter<MangaPage, PagesCache>(onItemClickListener), CoroutineScope, DisposableHandle {
private val job = SupervisorJob() private val job = SupervisorJob()
private val cache by inject<PagesCache>()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
@@ -22,7 +25,7 @@ class PagesThumbnailsAdapter(onItemClickListener: OnRecyclerItemClickListener<Ma
job.cancel() job.cancel()
} }
override fun getExtra(item: MangaPage, position: Int) = Unit override fun getExtra(item: MangaPage, position: Int) = cache
override fun onCreateViewHolder(parent: ViewGroup) = PageThumbnailHolder(parent, this) override fun onCreateViewHolder(parent: ViewGroup) = PageThumbnailHolder(parent, this)

View File

@@ -6,7 +6,7 @@ import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page.* import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
@@ -20,6 +20,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader { SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
private var job: Job? = null private var job: Job? = null
private var scrollToRestore = 0f
init { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(this)
@@ -34,6 +35,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
private fun doLoad(data: MangaPage, force: Boolean) { private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel() job?.cancel()
scrollToRestore = 0f
job = launch { job = launch {
layout_error.isVisible = false layout_error.isVisible = false
progressBar.isVisible = true progressBar.isVisible = true
@@ -51,17 +53,39 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
} }
} }
override fun onRecycled() {
job?.cancel()
ssiv.recycle()
}
fun getScrollY() = ssiv.center?.y ?: 0f
fun restoreScroll(scroll: Float) {
if (ssiv.isReady) {
ssiv.setScaleAndCenter(
ssiv.scale,
PointF(
ssiv.sWidth / 2f,
scroll
)
)
} else {
scrollToRestore = scroll
}
}
override fun onReady() { override fun onReady() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat() ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.setScaleAndCenter( ssiv.setScaleAndCenter(
ssiv.minScale, ssiv.minScale,
PointF( PointF(
ssiv.sWidth / 2f, ssiv.sWidth / 2f,
if (itemView.top < 0) { when {
ssiv.sHeight.toFloat() scrollToRestore != 0f -> scrollToRestore
} else { itemView.top < 0 -> ssiv.sHeight.toFloat()
0f else -> 0f
} }
) )
) )

Some files were not shown because too many files have changed in this diff Show More