Compare commits

..

1 Commits

Author SHA1 Message Date
Koitharu
5fe40ac17e Fix old api compatibility 2020-04-25 21:29:46 +03:00
670 changed files with 9521 additions and 17106 deletions

4
.idea/compiler.xml generated
View File

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

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
</component>
</project>

View File

@@ -1,14 +1,11 @@
<component name="ProjectDictionaryState">
<dictionary name="admin">
<words>
<w>amoled</w>
<w>chucker</w>
<w>desu</w>
<w>failsafe</w>
<w>koin</w>
<w>kotatsu</w>
<w>manga</w>
<w>snackbar</w>
<w>upsert</w>
<w>webtoon</w>
</words>

4
.idea/gradle.xml generated
View File

@@ -4,9 +4,10 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
@@ -14,6 +15,7 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

View File

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

6
.idea/kotlinc.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
</project>

31
.idea/misc.xml generated
View File

@@ -1,35 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/drawable/list_divider_material.xml" value="0.28512820512820514" />
<entry key="../../../../../../opt/usr/android-sdk/platforms/android-30/data/res/layout/simple_dropdown_item_1line.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/drawable/tab_indicator.xml" value="0.28512820512820514" />
<entry key="app/src/main/res/drawable/tabs_background.xml" value="0.28512820512820514" />
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
<entry key="app/src/main/res/layout-w600dp/fragment_list.xml" value="0.14635416666666667" />
<entry key="app/src/main/res/layout/dialog_favorite_categories.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/dialog_list_mode.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/fragment_chapters.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/layout/fragment_details.xml" value="0.26145833333333335" />
<entry key="app/src/main/res/layout/fragment_favourites.xml" value="0.26296296296296295" />
<entry key="app/src/main/res/layout/fragment_feed.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/fragment_list.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_branch.xml" value="0.24739583333333334" />
<entry key="app/src/main/res/layout/item_branch_dropdown.xml" value="0.25743589743589745" />
<entry key="app/src/main/res/layout/item_category_checkable.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_manga_grid.xml" value="0.26042632066728455" />
<entry key="app/src/main/res/layout/item_manga_list_details.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_page_thumb.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/item_recent.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/layout/sheet_pages.xml" value="0.2601851851851852" />
<entry key="app/src/main/res/menu/popup_category.xml" value="0.2601851851851852" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

12
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

9
.idea/vcs.xml generated
View File

@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list>
<option value="master" />
<option value="devel" />
<option value="legacy" />
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>

View File

@@ -1,11 +1,15 @@
language: android
dist: trusty
jdk:
- oraclejdk8
android:
components:
- android-30
- build-tools-30.0.3
- platform-tools-30.0.5
- tools
before_install:
- yes | sdkmanager "platforms;android-30"
- platform-tools-29.0.6
- build-tools-29.0.3
- android-29
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+
- google-gdk-license-.+
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug

View File

@@ -8,8 +8,6 @@ Kotatsu is a free and open source manga reader for Android.
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy)
### Main Features
* Online manga catalogues
@@ -21,14 +19,12 @@ Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/re
* Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader
* Notifications about new chapters
* Updates feed
* Global search
### Screenshots
| ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/80315102-3478db00-87fe-11ea-9ce8-4bbd1c254b2b.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/80315110-3f337000-87fe-11ea-95df-944c196b6667.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/80315121-49ee0500-87fe-11ea-8d9b-537a041bbf2f.png) |
| ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/75573590-d467f180-5a65-11ea-8338-a34af4679ed6.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/75573612-dcc02c80-5a65-11ea-9afb-293dadfb3cfd.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/75573621-e0ec4a00-5a65-11ea-92b9-72ab90281a2b.png) |
|---|---|---|
| ![Screenshot_20200226-210405](https://user-images.githubusercontent.com/8948226/80315130-55d9c700-87fe-11ea-8350-2c8452906eb7.png) | ![Screenshot_20200226-210151](https://user-images.githubusercontent.com/8948226/80315135-612cf280-87fe-11ea-984c-aa18567d5bbc.png) | ![Screenshot_20200226-210223](https://user-images.githubusercontent.com/8948226/80315146-6be78780-87fe-11ea-8439-ca1ca578172b.png) |
| ![Screenshot_20200226-210405](https://user-images.githubusercontent.com/8948226/75573629-e34ea400-5a65-11ea-86a1-4496032ac0f0.png) | ![Screenshot_20200226-210151](https://user-images.githubusercontent.com/8948226/75573632-e5186780-5a65-11ea-81b0-7c296157709c.png) | ![Screenshot_20200226-210223](https://user-images.githubusercontent.com/8948226/75573639-e6e22b00-5a65-11ea-84a6-6257f532fd2c.png) |
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
@@ -41,4 +37,4 @@ published by the Free Software Foundation, either version 3 of the License, or
### Disclaimer
The developers of this application does not have any affiliation with the content providers available.
The developers of this application does not have any affiliation with the content providers available.

View File

@@ -1,108 +1,101 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
compileSdkVersion 29
buildToolsVersion '29.0.3'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 30
minSdkVersion 16
maxSdkVersion 20
targetSdkVersion 29
versionCode gitCommits
versionName '1.0-rc3'
versionName '0.3'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
vectorDrawables.useSupportLibrary = true
kapt {
arguments {
arg 'room.schemaLocation', "$projectDir/schemas".toString()
arg('room.schemaLocation', "$projectDir/schemas".toString())
}
}
}
archivesBaseName = "kotatsu_${gitCommits}"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
}
release {
multiDexEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
lintOptions {
disable 'MissingTranslation'
abortOnError false
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = false
unitTests.returnDefaultValues = true
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=org.koin.core.component.KoinApiExtension'
]
}
androidExtensions {
experimental = true
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'androidx.core:core-ktx:1.5.0-beta03'
implementation 'androidx.activity:activity-ktx:1.2.1'
implementation 'androidx.fragment:fragment-ktx:1.3.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-service:2.3.0'
implementation 'androidx.lifecycle:lifecycle-process:2.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta02'
implementation 'androidx.core:core-ktx:1.3.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.2.4'
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.5.0'
implementation 'com.google.android.material:material:1.3.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0'
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.6'
implementation 'androidx.room:room-ktx:2.2.6'
kapt 'androidx.room:room-compiler:2.2.6'
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.10.0'
implementation 'com.github.moxy-community:moxy:2.1.2'
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
implementation 'com.github.moxy-community:moxy-material:2.1.2'
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'com.squareup.okio:okio:2.5.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0'
implementation 'org.koin:koin-android:2.2.2'
implementation 'org.koin:koin-androidx-viewmodel:2.2.2'
implementation 'io.coil-kt:coil-base:1.1.1'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.9.5'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
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.2'
testImplementation 'org.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.2'
testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722'
}

View File

@@ -9,5 +9,4 @@
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
public <init>(...);
}
-dontwarn okhttp3.internal.platform.ConscryptPlatform
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#E6000A">
<group android:scaleX="0.40188664"
android:scaleY="0.40188664"
android:translateX="32.90095"
android:translateY="18.7272">
<group android:translateY="139.39206">
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
android:fillColor="#E6000A"/>
</group>
</group>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<string name="app_name" translatable="false">Kotatsu Dev</string>
</resources>

View File

@@ -23,7 +23,7 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
<activity android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -32,66 +32,50 @@
android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" />
</activity>
<activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
<activity android:name=".ui.details.MangaDetailsActivity">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
</activity>
<activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity">
<intent-filter>
<action android:name="${applicationId}.action.READ_MANGA" />
</intent-filter>
</activity>
<activity android:name=".ui.reader.ReaderActivity" />
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:name=".ui.search.SearchActivity"
android:label="@string/search" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:name=".ui.settings.SettingsActivity"
android:label="@string/settings" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:name=".ui.reader.SimpleSettingsActivity"
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="org.koitharu.kotatsu.browser.BrowserActivity" />
<activity android:name=".ui.browser.BrowserActivity" />
<activity
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:name=".ui.utils.CrashActivity"
android:label="@string/error_occurred"
android:theme="@android:style/Theme.DeviceDefault"
android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
android:label="@string/favourites_categories"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
android:label="@string/search" />
<activity
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
android:windowSoftInputMode="adjustResize" />
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:label="@string/favourites_categories" />
<service
android:name="org.koitharu.kotatsu.download.DownloadService"
android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
android:name=".ui.widget.recent.RecentWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
android:name=".ui.search.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" />
<provider
@@ -103,35 +87,24 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"
<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"
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_shelf" />
</receiver>
<receiver
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:exported="true"
<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"
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" />
</receiver>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application>
</manifest>

View File

@@ -1,60 +1,57 @@
package org.koitharu.kotatsu
import android.app.Application
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import org.koin.android.ext.android.get
import androidx.room.Room
import coil.Coil
import coil.ImageLoader
import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.parser.parserModule
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.AppCrashHandler
import org.koitharu.kotatsu.core.ui.uiModule
import org.koitharu.kotatsu.details.detailsModule
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.favouritesModule
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.historyModule
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit
class KotatsuApp : Application() {
private val cookieJar by lazy {
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
}
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
ChuckerCollector(applicationContext)
}
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build()
)
}
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
initKoin()
initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
if (BuildConfig.DEBUG) {
initErrorHandler()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
val widgetUpdater = WidgetUpdater(applicationContext)
FavouritesRepository.subscribe(widgetUpdater)
HistoryRepository.subscribe(widgetUpdater)
@@ -62,25 +59,67 @@ class KotatsuApp : Application() {
private fun initKoin() {
startKoin {
androidContext(this@KotatsuApp)
androidLogger()
androidContext(applicationContext)
modules(
networkModule,
databaseModule,
githubModule,
uiModule,
parserModule,
mainModule,
searchModule,
localModule,
favouritesModule,
historyModule,
remoteListModule,
detailsModule,
trackerModule,
settingsModule,
readerModule,
appWidgetModule
module {
factory {
okHttp()
.cache(CacheUtils.createHttpCache(applicationContext))
.build()
}
single {
mangaDb().build()
}
single {
MangaLoaderContext()
}
factory {
AppSettings(applicationContext)
}
single {
PagesCache(applicationContext)
}
}
)
}
}
private fun initCoil() {
Coil.setDefaultImageLoader(ImageLoader(applicationContext) {
okHttpClient {
okHttp()
.cache(CoilUtils.createDefaultCache(applicationContext))
.build()
}
componentRegistry {
add(CbzFetcher())
}
})
}
private fun initErrorHandler() {
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
chuckerCollector.onError("CRASH", e)
exceptionHandler?.uncaughtException(t, e)
}
}
private fun okHttp() = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
addInterceptor(UserAgentInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
}
}
private fun mangaDb() = Room.databaseBuilder(
applicationContext,
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(Migration1To2, Migration2To3, Migration3To4)
}

View File

@@ -1,33 +0,0 @@
package org.koitharu.kotatsu.base.domain
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga
data class MangaIntent(
val manga: Manga?,
val mangaId: Long,
val uri: Uri?
) {
companion object {
fun from(intent: Intent?) = MangaIntent(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
fun from(args: Bundle?) = MangaIntent(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
}
}

View File

@@ -1,71 +0,0 @@
package org.koitharu.kotatsu.base.domain
import okhttp3.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.await
open class MangaLoaderContext(
private val okHttp: OkHttpClient,
private val cookieJar: CookieJar
) : KoinComponent {
suspend fun httpGet(url: String): Response {
val request = Request.Builder()
.get()
.url(url)
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
form: Map<String, String>
): Response {
val body = FormBody.Builder()
form.forEach { (k, v) ->
body.addEncoded(k, v)
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
payload: String
): Response {
val body = FormBody.Builder()
payload.split('&').forEach {
val pos = it.indexOf('=')
if (pos != -1) {
val k = it.substring(0, pos)
val v = it.substring(pos + 1)
body.addEncoded(k, v)
}
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
fun insertCookies(domain: String, vararg cookies: String) {
val url = HttpUrl.Builder()
.scheme(SCHEME_HTTP)
.host(domain)
.build()
cookieJar.saveFromResponse(url, cookies.mapNotNull {
Cookie.parse(url, it)
})
}
private companion object {
private const val SCHEME_HTTP = "http"
}
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.base.domain
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory {
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val hidden = settings.hiddenSources
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if (includeHidden) {
sorted
} else {
sorted.filterNot { x ->
x.name in hidden
}
}
}
}

View File

@@ -1,70 +0,0 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.annotation.WorkerThread
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream
import java.util.zip.ZipFile
object MangaUtils : KoinComponent {
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CacheUtils.CONTROL_DISABLED)
.build()
client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream())
}
}
return size.width * 2 < size.height
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
return null
}
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}

View File

@@ -1,47 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
val binding = onInflateView(inflater, null)
viewBinding = binding
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root)
.also(::onBuildDialog)
.create()
}
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = viewBinding?.root
@CallSuper
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}

View File

@@ -1,113 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.viewbinding.ViewBinding
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.getThemeColor
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
protected lateinit var binding: B
private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(this, supportFragmentManager)
}
override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) {
setTheme(R.style.AppTheme_Amoled)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setupToolbar()
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(view: View?) {
super.setContentView(view)
setupToolbar()
}
protected fun setContentView(binding: B) {
this.binding = binding
super.setContentView(binding.root)
(binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
onWindowInsetsChanged(insets.getInsets(WindowInsetsCompat.Type.systemBars()))
return insets
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
onBackPressed()
true
} else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
ActivityCompat.recreate(this)
return true
}
return super.onKeyDown(keyCode, event)
}
protected abstract fun onWindowInsetsChanged(insets: Insets)
private fun setupToolbar() {
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
}
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
val insets = ViewCompat.getRootWindowInsets(binding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
view?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
window?.statusBarColor = ContextCompat.getColor(this, R.color.grey_dark)
}
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
window?.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
}
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
isTaskRoot &&
supportFragmentManager.backStackEntryCount == 0
) {
finishAfterTransition()
} else {
super.onBackPressed()
}
}
}

View File

@@ -1,43 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialog
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
abstract class BaseBottomSheet<B : ViewBinding> :
BottomSheetDialogFragment() {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = onInflateView(inflater, container)
viewBinding = binding
return binding.root
}
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, theme)
} else super.onCreateDialog(savedInstanceState)
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}

View File

@@ -1,66 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = onInflateView(inflater, container)
viewBinding = binding
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(view, this)
}
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) {
super.onAttach(context)
getTitle()?.let {
activity?.title = it
}
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
onWindowInsetsChanged(insets.getInsets(WindowInsetsCompat.Type.systemBars()))
return insets
}
protected fun bindingOrNull() = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
protected abstract fun onWindowInsetsChanged(insets: Insets)
}

View File

@@ -1,57 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(window) {
statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
}
showSystemUI()
}
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
@Suppress("DEPRECATION")
private companion object {
const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
}

View File

@@ -1,39 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
protected val settings by inject<AppSettings>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listView.clipToPadding = false
ViewCompat.setOnApplyWindowInsetsListener(view, this)
}
override fun onResume() {
super.onResume()
activity?.setTitle(titleId)
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
listView.updatePadding(
left = systemBars.left,
right = systemBars.right,
bottom = systemBars.bottom
)
return insets
}
}

View File

@@ -1,5 +0,0 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LifecycleService
abstract class BaseService : LifecycleService()

View File

@@ -1,44 +0,0 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.SingleLiveEvent
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseViewModel : ViewModel() {
val onError = SingleLiveEvent<Throwable>()
val isLoading = MutableLiveData(false)
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
isLoading.postValue(true)
try {
block()
} finally {
isLoading.postValue(false)
}
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) {
throwable.printStackTrace()
}
if (throwable !is CancellationException) {
onError.postCall(throwable)
}
}
}

View File

@@ -1,95 +0,0 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context)
private val delegate = MaterialAlertDialogBuilder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val checked = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setSingleChoiceItems(adapter, checked) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
}
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setNegativeButton(@StringRes textId: Int): Builder {
delegate.setNegativeButton(textId, null)
return this
}
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(context: Context) : BaseAdapter() {
val volumes = getAvailableVolumes(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: parent.inflate(R.layout.item_storage)
val item = volumes[position]
val binding = ItemStorageBinding.bind(view)
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
}
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = volumes[position].first.absolutePath.longHashCode()
override fun getCount() = volumes.size
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
private companion object {
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
return LocalMangaRepository.getAvailableStorageDirs(context).map {
it to it.getStorageName(context)
}
}
}
}

View File

@@ -1,89 +0,0 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor(
private val delegate: AlertDialog
) : DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context) {
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setHint(@StringRes hintResId: Int): Builder {
binding.inputLayout.hint = binding.root.context.getString(hintResId)
return this
}
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
with(binding.inputLayout) {
counterMaxLength = maxLength
isCounterEnabled = maxLength > 0
}
if (strict && maxLength > 0) {
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
}
return this
}
fun setInputType(inputType: Int): Builder {
binding.inputEdit.inputType = inputType
return this
}
fun setText(text: String): Builder {
binding.inputEdit.setText(text)
binding.inputEdit.setSelection(text.length)
return this
}
fun setPositiveButton(
@StringRes textId: Int,
listener: (DialogInterface, String) -> Unit
): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, binding.inputEdit.text.toString().orEmpty())
}
return this
}
fun setNegativeButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener? = null
): Builder {
delegate.setNegativeButton(textId, listener)
return this
}
fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder {
delegate.setOnCancelListener(listener)
return this
}
fun create() =
TextInputDialog(delegate.create())
}
}

View File

@@ -1,28 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koin.core.component.KoinComponent
@Deprecated("")
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
RecyclerView.ViewHolder(binding.root), KoinComponent {
var boundData: T? = null
private set
val context get() = itemView.context!!
fun bind(data: T, extra: E) {
boundData = data
onBind(data, extra)
}
fun requireData(): T {
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E)
}

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
interface OnListItemClickListener<I> {
fun onItemClick(item: I, view: View)
fun onItemLongClick(item: I, view: View) = false
}

View File

@@ -1,18 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
class PaginationScrollListener(offset: Int, private val callback: Callback) :
BoundsScrollListener(0, offset) {
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
override fun onScrolledToEnd(recyclerView: RecyclerView) {
callback.onScrolledToEnd()
}
interface Callback {
fun onScrolledToEnd()
}
}

View File

@@ -1,18 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.view.View
import androidx.annotation.Px
import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.set(spacing, spacing, spacing, spacing)
}
}

View File

@@ -1,96 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.view.children
import com.google.android.material.R
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.chipGroupStyle
) : ChipGroup(context, attrs, defStyleAttr) {
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private var chipOnClickListener = OnClickListener {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
var onChipClickListener: OnChipClickListener? = null
set(value) {
field = value
val isChipClickable = value != null
children.forEach { it.isClickable = isChipClickable }
}
override fun requestLayout() {
if (isLayoutSuppressedCompat) {
isLayoutCalledOnSuppressed = true
} else {
super.requestLayout()
}
}
fun setChips(items: List<ChipModel>) {
suppressLayoutCompat(true)
try {
for ((i, model) in items.withIndex()) {
val chip = getChildAt(i) as Chip? ?: addChip()
bindChip(chip, model)
}
for (i in items.size until childCount) {
removeViewAt(i)
}
} finally {
suppressLayoutCompat(false)
}
}
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
if (model.icon == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.setChipIconResource(model.icon)
}
chip.tag = model.data
}
private fun addChip(): Chip {
val chip = Chip(context)
chip.setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
chip.isCloseIconVisible = false
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isClickable = onChipClickListener != null
addView(chip)
return chip
}
private fun suppressLayoutCompat(suppress: Boolean) {
isLayoutSuppressedCompat = suppress
if (!suppress) {
if (isLayoutCalledOnSuppressed) {
requestLayout()
isLayoutCalledOnSuppressed = false
}
}
}
data class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
)
fun interface OnChipClickListener {
fun onChipClick(chip: Chip, data: Any?)
}
}

View File

@@ -1,45 +0,0 @@
package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebResourceResponse
import android.webkit.WebView
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.core.network.WebViewClientCompat
class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat(), KoinComponent {
private val okHttp by inject<OkHttpClient>()
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
callback.onLoadingStateChanged(isLoading = false)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
callback.onLoadingStateChanged(isLoading = true)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
override fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
return runCatching {
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()
)
}.getOrNull()
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
interface CloudFlareCallback {
fun onPageLoaded()
fun onCheckPassed()
}

View File

@@ -1,47 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.WebView
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.WebViewClientCompat
class CloudFlareClient(
private val cookieJar: AndroidCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String
) : WebViewClientCompat() {
init {
cookieJar.remove(targetUrl, CF_UID, CF_CLEARANCE)
}
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
checkClearance()
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
callback.onPageLoaded()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
callback.onPageLoaded()
}
private fun checkClearance() {
val cookies = cookieJar.loadForRequest(targetUrl.toHttpUrl())
if (cookies.any { it.name == CF_CLEARANCE }) {
callback.onCheckPassed()
}
}
private companion object {
const val CF_UID = "__cfduid"
const val CF_CLEARANCE = "cf_clearance"
}
}

View File

@@ -1,93 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private val url by stringArgument(ARG_URL)
private val pendingResult = Bundle(1)
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent
}
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
if (url.isNullOrEmpty()) {
dismissAllowingStateLoss()
} else {
binding.webView.loadUrl(url.orEmpty())
}
}
override fun onDestroyView() {
binding.webView.stopLoading()
super.onDestroyView()
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setNegativeButton(android.R.string.cancel, null)
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
}
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(TAG, pendingResult)
super.onDismiss(dialog)
}
override fun onPageLoaded() {
bindingOrNull()?.progressBar?.isInvisible = true
}
override fun onCheckPassed() {
pendingResult.putBoolean(EXTRA_RESULT, true)
dismiss()
}
companion object {
const val TAG = "CloudFlareDialog"
const val EXTRA_RESULT = "result"
private const val ARG_URL = "url"
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
putString(ARG_URL, url)
}
}
}

View File

@@ -1,51 +0,0 @@
package org.koitharu.kotatsu.core.backup
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.MutableZipFile
import org.koitharu.kotatsu.utils.ext.format
import java.io.File
import java.util.*
class BackupArchive(file: File) : MutableZipFile(file) {
init {
if (!dir.exists()) {
dir.mkdirs()
}
}
suspend fun put(entry: BackupEntry) {
put(entry.name, entry.data.toString(2))
}
suspend fun getEntry(name: String): BackupEntry {
val json = withContext(Dispatchers.Default) {
JSONArray(getContent(name))
}
return BackupEntry(name, json)
}
companion object {
private const val DIR_BACKUPS = "backups"
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}
dir.mkdirs()
val filename = buildString {
append(context.getString(R.string.app_name).toLowerCase(Locale.ROOT))
append('_')
append(Date().format("ddMMyyyy"))
append(".bak")
}
BackupArchive(File(dir, filename))
}
}
}

View File

@@ -1,17 +0,0 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
data class BackupEntry(
val name: String,
val data: JSONArray
) {
companion object Names {
const val INDEX = "index"
const val HISTORY = "history"
const val CATEGORIES = "categories"
const val FAVOURITES = "favourites"
}
}

View File

@@ -1,135 +0,0 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
class BackupRepository(private val db: MangaDatabase) {
suspend fun dumpHistory(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
while (true) {
val history = db.historyDao.findAll(offset, PAGE_SIZE)
if (history.isEmpty()) {
break
}
offset += history.size
for (item in history) {
val manga = item.manga.toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
manga.put("tags", tags)
val json = item.history.toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val categories = db.favouriteCategoriesDao.findAll()
for (item in categories) {
entry.data.put(item.toJson())
}
return entry
}
suspend fun dumpFavourites(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
while (true) {
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
if (favourites.isEmpty()) {
break
}
offset += favourites.size
for (item in favourites) {
val manga = item.manga.toJson()
val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) }
manga.put("tags", tags)
val json = item.favourite.toJson()
json.put("manga", manga)
entry.data.put(json)
}
}
return entry
}
suspend fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE)
json.put("created_at", System.currentTimeMillis())
entry.data.put(json)
return entry
}
private fun MangaEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("alt_title", altTitle)
jo.put("url", url)
jo.put("public_url", publicUrl)
jo.put("rating", rating)
jo.put("cover_url", coverUrl)
jo.put("large_cover_url", largeCoverUrl)
jo.put("state", state)
jo.put("author", author)
jo.put("source", source)
return jo
}
private fun TagEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("id", id)
jo.put("title", title)
jo.put("key", key)
jo.put("source", source)
return jo
}
private fun HistoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("created_at", createdAt)
jo.put("updated_at", updatedAt)
jo.put("chapter_id", chapterId)
jo.put("page", page)
jo.put("scroll", scroll)
return jo
}
private fun FavouriteCategoryEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
jo.put("sort_key", sortKey)
jo.put("title", title)
return jo
}
private fun FavouriteEntity.toJson(): JSONObject {
val jo = JSONObject()
jo.put("manga_id", mangaId)
jo.put("category_id", categoryId)
jo.put("created_at", createdAt)
return jo
}
private companion object {
const val PAGE_SIZE = 10
}
}

View File

@@ -1,39 +0,0 @@
package org.koitharu.kotatsu.core.backup
class CompositeResult {
private var successCount: Int = 0
private val errors = ArrayList<Throwable?>()
val size: Int
get() = successCount + errors.size
val failures: List<Throwable>
get() = errors.filterNotNull()
val isAllSuccess: Boolean
get() = errors.none { it != null }
val isAllFailed: Boolean
get() = successCount == 0 && errors.isNotEmpty()
operator fun plusAssign(result: Result<*>) {
when {
result.isSuccess -> successCount++
result.isFailure -> errors.add(result.exceptionOrNull())
}
}
operator fun plusAssign(other: CompositeResult) {
this.successCount += other.successCount
this.errors += other.errors
}
operator fun plus(other: CompositeResult): CompositeResult {
val result = CompositeResult()
result.successCount = this.successCount + other.successCount
result.errors.addAll(this.errors)
result.errors.addAll(other.errors)
return result
}
}

View File

@@ -1,110 +0,0 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.utils.ext.getStringOrNull
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.map
class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
parseTag(it)
}
val history = parseHistory(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.historyDao.upsert(history)
}
}
}
return result
}
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val category = parseCategory(item)
result += runCatching {
db.favouriteCategoriesDao.upsert(category)
}
}
return result
}
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
parseTag(it)
}
val favourite = parseFavourite(item)
result += runCatching {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.favouritesDao.upsert(favourite)
}
}
}
return result
}
private fun parseManga(json: JSONObject) = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
source = json.getString("source")
)
private fun parseTag(json: JSONObject) = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source")
)
private fun parseHistory(json: JSONObject) = HistoryEntity(
mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat()
)
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"),
title = json.getString("title")
)
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
mangaId = json.getLong("manga_id"),
categoryId = json.getLong("category_id"),
createdAt = json.getLong("created_at")
)
}

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.migrations.*
val databaseModule
get() = module {
single {
Room.databaseBuilder(
androidContext(),
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7()
).addCallback(
DatabasePrePopulateCallback(androidContext().resources)
).build()
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.core.db
import android.content.res.Resources
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.R
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title) VALUES (?,?,?)",
arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later))
)
}
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
@Dao
abstract class FavouriteCategoriesDao {
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy")
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
abstract suspend fun delete(id: Long)
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String)
}

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao
abstract class FavouritesDao {
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY :orderBy LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract suspend fun find(id: Long): FavouriteManga?
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun add(favourite: FavouriteEntity)
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
abstract suspend fun delete(categoryId: Long, mangaId: Long)
}

View File

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

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.MangaEntity

View File

@@ -2,21 +2,13 @@ package org.koitharu.kotatsu.core.db
import androidx.room.Database
import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class
], version = 7
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
], version = 4
)
abstract class MangaDatabase : RoomDatabase() {
@@ -33,6 +25,4 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity

View File

@@ -1,22 +1,22 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
abstract class TagsDao {
interface TagsDao {
@Query("SELECT * FROM tags")
abstract suspend fun getAllTags(): List<TagEntity>
suspend fun getAllTags(): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(tag: TagEntity): Long
suspend fun insert(tag: TagEntity): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun update(tag: TagEntity): Int
suspend fun update(tag: TagEntity): Int
@Transaction
open suspend fun upsert(tags: Iterable<TagEntity>) {
suspend fun upsert(tags: Iterable<TagEntity>) {
tags.forEach { tag ->
if (update(tag) <= 0) {
insert(tag)

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.db.dao
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackEntity
@@ -25,13 +25,11 @@ abstract class TracksDao {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
@Transaction
open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) {
insert(entity)
}
}
}

View File

@@ -1,28 +0,0 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogWithManga
@Dao
interface TrackLogsDao {
@Transaction
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
suspend fun findAll(offset: Int, limit: Int): List<TrackLogWithManga>
@Query("DELETE FROM track_logs")
suspend fun clear()
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.favourites.data
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
@@ -11,14 +11,12 @@ data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "title") val title: String
) {
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
id = id ?: categoryId.toLong(),
title = title,
sortKey = sortKey,
createdAt = Date(createdAt)
)
}

View File

@@ -1,9 +1,8 @@
package org.koitharu.kotatsu.favourites.data
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [

View File

@@ -1,11 +1,8 @@
package org.koitharu.kotatsu.favourites.data
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
data class FavouriteManga(
@Embedded val favourite: FavouriteEntity,

View File

@@ -1,10 +1,9 @@
package org.koitharu.kotatsu.history.data
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.model.MangaHistory
import java.util.*
@@ -33,6 +32,6 @@ data class HistoryEntity(
updatedAt = Date(updatedAt),
chapterId = chapterId,
page = page,
scroll = scroll.toInt()
scroll = scroll
)
}

View File

@@ -1,11 +1,8 @@
package org.koitharu.kotatsu.history.data
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
data class HistoryWithManga(
@Embedded val history: HistoryEntity,

View File

@@ -15,7 +15,6 @@ data class MangaEntity(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
@ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
@@ -31,7 +30,6 @@ data class MangaEntity(
state = this.state?.let { MangaState.valueOf(it) },
rating = this.rating,
url = this.url,
publicUrl = this.publicUrl,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
@@ -44,7 +42,6 @@ data class MangaEntity(
fun from(manga: Manga) = MangaEntity(
id = manga.id,
url = manga.url,
publicUrl = manga.publicUrl,
source = manga.source.name,
largeCoverUrl = manga.largeCoverUrl,
coverUrl = manga.coverUrl,

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet
data class MangaWithTags(
@Embedded val manga: MangaEntity,
@@ -15,7 +14,7 @@ data class MangaWithTags(
val tags: List<TagEntity>
) {
fun toManga() = manga.toManga(tags.mapToSet {
fun toManga() = manga.toManga(tags.map {
it.toMangaTag()
})
}.toSet())
}

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
)
]
)
data class TrackEntity(
data class TrackEntity (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int,

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "track_logs", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class TrackLogEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "chapters") val chapters: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

View File

@@ -1,31 +0,0 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
data class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id"
)
val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
) {
fun toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.mapToSet { x -> x.toMangaTag() }),
createdAt = Date(trackLog.createdAt)
)
}

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration1To2 : Migration(1, 2) {
object Migration1To2 : Migration(1, 2) {
/**
* Adding foreign keys
*/

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration4To5 : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration5To6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
}
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration6To7 : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
}
}

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
class AuthRequiredException(
val url: String
) : RuntimeException("Authorization required"), ResolvableException {
@StringRes
override val resolveTextId: Int = R.string.sign_in
}

View File

@@ -1,14 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
import androidx.annotation.StringRes
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
class CloudFlareProtectedException(
val url: String
) : IOException("Protected by CloudFlare"), ResolvableException {
@StringRes
override val resolveTextId: Int = R.string.captcha_solve
}

View File

@@ -1,3 +1,5 @@
package org.koitharu.kotatsu.core.exceptions
import java.lang.NullPointerException
class MangaNotFoundException(s: String? = null) : RuntimeException(s)

View File

@@ -1,4 +1,3 @@
package org.koitharu.kotatsu.core.exceptions
class ParseException(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause)
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)

View File

@@ -1,3 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
class WrongPasswordException : SecurityException()

View File

@@ -1,40 +0,0 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.util.ArrayMap
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class ExceptionResolver(
private val lifecycleOwner: LifecycleOwner,
private val fm: FragmentManager
) {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
suspend fun resolve(e: ResolvableException): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url)
is AuthRequiredException -> false //TODO
else -> false
}
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont ->
val dialog = CloudFlareDialog.newInstance(url)
fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
}
dialog.show(fm, CloudFlareDialog.TAG)
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismiss()
}
}
}

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.core.exceptions.resolve
interface ResolvableException {
val resolveTextId: Int
}

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.github
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.android.parcel.Parcelize
@Parcelize
data class AppVersion(
@@ -9,6 +9,5 @@ data class AppVersion(
val name: String,
val url: String,
val apkSize: Long,
val apkUrl: String,
val description: String
val apkUrl: String
) : Parcelable

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.core.github
import org.koin.dsl.module
val githubModule
get() = module {
single {
GithubRepository(get())
}
}

View File

@@ -2,10 +2,14 @@ package org.koitharu.kotatsu.core.github
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.parseJson
class GithubRepository(private val okHttp: OkHttpClient) {
class GithubRepository : KoinComponent {
private val okHttp by inject<OkHttpClient>()
suspend fun getLatestVersion(): AppVersion {
val request = Request.Builder()
@@ -18,8 +22,7 @@ class GithubRepository(private val okHttp: OkHttpClient) {
url = json.getString("html_url"),
name = json.getString("name").removePrefix("v"),
apkSize = asset.getLong("size"),
apkUrl = asset.getString("browser_download_url"),
description = json.getString("body")
apkUrl = asset.getString("browser_download_url")
)
}
}

View File

@@ -32,6 +32,7 @@ data class VersionId(
companion object {
@JvmStatic
private fun variantWeight(variantType: String) =
when (variantType.toLowerCase(Locale.ROOT)) {
"a", "alpha" -> 1
@@ -41,6 +42,7 @@ data class VersionId(
else -> 0
}
@JvmStatic
fun parse(versionName: String): VersionId {
val parts = versionName.substringBeforeLast('-').split('.')
val variant = versionName.substringAfterLast('-', "")

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.local.data
package org.koitharu.kotatsu.core.local
enum class Cache(val dir: String) {

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.local.data
package org.koitharu.kotatsu.core.local
import android.net.Uri
import android.webkit.MimeTypeMap
import coil.bitmap.BitmapPool
import coil.bitmappool.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.local
import java.io.File
import java.io.FilenameFilter
class CbzFilter : FilenameFilter {
override fun accept(dir: File, name: String) =
name.endsWith(".cbz", ignoreCase = true) || name.endsWith(".zip", ignoreCase = true)
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.local.data
package org.koitharu.kotatsu.core.local
import android.content.Context
import com.tomclaw.cache.DiskLruCache
@@ -7,20 +7,17 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
class PagesCache(context: Context) {
private val cacheDir = context.externalCacheDir ?: context.cacheDir
private val lruCache =
DiskLruCache.create(cacheDir.sub(Cache.PAGES.dir), FileSizeUtils.mbToBytes(200))
private val lruCache = DiskLruCache.create(cacheDir.sub(Cache.PAGES.dir), FileSizeUtils.mbToBytes(200))
operator fun get(url: String): File? {
return lruCache.get(url)?.takeIfReadable()
}
@Deprecated("Useless lambda")
fun put(url: String, writer: (OutputStream) -> Unit): File {
val file = cacheDir.sub(url.longHashCode().toString())
file.outputStream().use(writer)
@@ -28,14 +25,4 @@ class PagesCache(context: Context) {
file.delete()
return res
}
fun put(url: String, inputStream: InputStream): File {
val file = cacheDir.sub(url.longHashCode().toString())
file.outputStream().use { out ->
inputStream.copyTo(out)
}
val res = lruCache.put(url, file)
file.delete()
return res
}
}

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