Compare commits
1 Commits
testbuild-
...
v0.4-legac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe40ac17e |
@@ -1,19 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 4
|
|
||||||
indent_style = tab
|
|
||||||
insert_final_newline = false
|
|
||||||
max_line_length = 120
|
|
||||||
tab_width = 4
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
|
||||||
disabled_rules=no-wildcard-imports,no-unused-imports
|
|
||||||
|
|
||||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
|
||||||
ij_continuation_indent_size = 4
|
|
||||||
|
|
||||||
[{*.kt,*.kts}]
|
|
||||||
ij_kotlin_allow_trailing_comma = true
|
|
||||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
|
||||||
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
|||||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
custom: ["https://money.yandex.ru/to/410012543938752"]
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -3,9 +3,7 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
/.idea/caches
|
||||||
/.idea/libraries
|
/.idea/libraries
|
||||||
/.idea/dictionaries
|
|
||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
|
|||||||
1
.idea/codeStyles/Project.xml
generated
@@ -23,7 +23,6 @@
|
|||||||
</option>
|
</option>
|
||||||
</AndroidXmlCodeStyleSettings>
|
</AndroidXmlCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="CMake">
|
<codeStyleSettings language="CMake">
|
||||||
|
|||||||
4
.idea/compiler.xml
generated
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel>
|
||||||
|
<module name="Kotatsu.app" target="1.8" />
|
||||||
|
</bytecodeTargetLevel>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
17
.idea/deploymentTargetDropDown.xml
generated
@@ -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>
|
|
||||||
13
.idea/dictionaries/admin.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="admin">
|
||||||
|
<words>
|
||||||
|
<w>chucker</w>
|
||||||
|
<w>desu</w>
|
||||||
|
<w>koin</w>
|
||||||
|
<w>kotatsu</w>
|
||||||
|
<w>manga</w>
|
||||||
|
<w>upsert</w>
|
||||||
|
<w>webtoon</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
6
.idea/gradle.xml
generated
@@ -4,16 +4,18 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="PLATFORM" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="Android Studio default JDK" />
|
<option name="gradleJvm" value="1.8" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
<option name="useQualifiedModuleNames" value="true" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
3
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,9 +1,6 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<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="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
10
.idea/jarRepositories.xml
generated
@@ -31,15 +31,5 @@
|
|||||||
<option name="name" value="maven2" />
|
<option name="name" value="maven2" />
|
||||||
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
|
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="MavenRepo" />
|
|
||||||
<option name="name" value="MavenRepo" />
|
|
||||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="maven2" />
|
|
||||||
<option name="name" value="maven2" />
|
|
||||||
<option name="url" value="https://maven.pkg.github.com/nv95/kotatsu-parsers" />
|
|
||||||
</remote-repository>
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
.idea/kotlinc.xml
generated
@@ -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>
|
|
||||||
7
.idea/ktlint.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KtlintProjectConfiguration">
|
|
||||||
<androidMode>true</androidMode>
|
|
||||||
<treatAsErrors>false</treatAsErrors>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/vcs.xml
generated
@@ -1,14 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="GitSharedSettings">
|
|
||||||
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
|
||||||
<list>
|
|
||||||
<option value="master" />
|
|
||||||
<option value="devel" />
|
|
||||||
<option value="legacy" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
14
.travis.yml
@@ -1,11 +1,15 @@
|
|||||||
language: android
|
language: android
|
||||||
dist: trusty
|
dist: trusty
|
||||||
|
jdk:
|
||||||
|
- oraclejdk8
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
- android-30
|
|
||||||
- build-tools-30.0.3
|
|
||||||
- platform-tools-30.0.5
|
|
||||||
- tools
|
- tools
|
||||||
before_install:
|
- platform-tools-29.0.6
|
||||||
- yes | sdkmanager "platforms;android-30"
|
- 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
|
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
||||||
41
README.md
@@ -2,48 +2,29 @@
|
|||||||
|
|
||||||
Kotatsu is a free and open source manga reader for Android.
|
Kotatsu is a free and open source manga reader for Android.
|
||||||
|
|
||||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
|
  [](https://travis-ci.org/nv95/Kotatsu)  [](http://4pda.ru/forum/index.php?showtopic=697669)
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
Latest release: [get here](https://github.com/nv95/Kotatsu/releases/latest)
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
|
|
||||||
|
|
||||||
Download APK from Github Releases:
|
|
||||||
|
|
||||||
- [Latest release](https://github.com/nv95/Kotatsu/releases/latest)
|
|
||||||
- [Legacy build](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy) (with Android 4.1+ support)
|
|
||||||
|
|
||||||
### Main Features
|
### Main Features
|
||||||
|
|
||||||
* Online manga catalogues
|
* Online manga catalogues
|
||||||
* Search manga by name and genre
|
* Search manga by name and genre
|
||||||
* Reading history
|
* Reading history
|
||||||
* Favourites organized by user-defined categories
|
* Favourites with custom categories
|
||||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
* Saving manga and reading it offline
|
||||||
* Tablet-optimized material design UI
|
* Tablet-optimized modern UI
|
||||||
|
* Reading third-party comics from CBZ
|
||||||
* Standard and Webtoon-optimized reader
|
* Standard and Webtoon-optimized reader
|
||||||
* Notifications about new chapters with updates feed
|
* Notifications about new chapters
|
||||||
* Available in multiple languages
|
|
||||||
* Password protect access to the app
|
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
|---|---|---|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|
|
||||||
|  |  |
|
|
||||||
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
|
|
||||||
|
|
||||||
### Localization
|
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/kotatsu/">
|
|
||||||
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, please head over to the Weblate <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
|
|
||||||
|
|
||||||
### License
|
### License
|
||||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
@@ -56,4 +37,4 @@ published by the Free Software Foundation, either version 3 of the License, or
|
|||||||
|
|
||||||
### Disclaimer
|
### 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.
|
||||||
152
app/build.gradle
@@ -1,119 +1,101 @@
|
|||||||
plugins {
|
apply plugin: 'com.android.application'
|
||||||
id 'com.android.application'
|
apply plugin: 'kotlin-android'
|
||||||
id 'kotlin-android'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
id 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
id 'kotlin-parcelize'
|
|
||||||
}
|
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||||
|
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 29
|
||||||
buildToolsVersion '32.0.0'
|
buildToolsVersion '29.0.3'
|
||||||
namespace 'org.koitharu.kotatsu'
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 16
|
||||||
targetSdkVersion 32
|
maxSdkVersion 20
|
||||||
versionCode 402
|
targetSdkVersion 29
|
||||||
versionName '3.1.1'
|
versionCode gitCommits
|
||||||
generatedDensities = []
|
versionName '0.3'
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
|
|
||||||
|
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
arg 'room.schemaLocation', "$projectDir/schemas".toString()
|
arg('room.schemaLocation', "$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
archivesBaseName = "kotatsu_${gitCommits}"
|
||||||
debug {
|
|
||||||
applicationIdSuffix = '.debug'
|
|
||||||
}
|
|
||||||
release {
|
|
||||||
multiDexEnabled false
|
|
||||||
minifyEnabled true
|
|
||||||
shrinkResources true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
sourceSets {
|
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
|
||||||
}
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
freeCompilerArgs += [
|
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
|
||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
lint {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = '.debug'
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
abortOnError false
|
abortOnError false
|
||||||
disable 'MissingTranslation', 'PrivateResource'
|
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources = true
|
unitTests.includeAndroidResources = true
|
||||||
unitTests.returnDefaultValues = false
|
unitTests.returnDefaultValues = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
implementation('com.github.nv95:kotatsu-parsers:8e23a7fcd4') {
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
exclude group: 'org.json', module: 'json'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||||
}
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
|
implementation 'androidx.core:core-ktx:1.3.0-rc01'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
|
implementation 'androidx.fragment:fragment-ktx:1.2.4'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.2.0-beta01'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
|
implementation 'androidx.work:work-runtime-ktx:2.3.4'
|
||||||
|
implementation 'com.google.android.material:material:1.2.0-alpha06'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.room:room-runtime:2.2.5'
|
||||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
implementation 'androidx.room:room-ktx:2.2.5'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
kapt 'androidx.room:room-compiler:2.2.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
|
||||||
implementation 'com.google.android.material:material:1.6.0-beta01'
|
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
|
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.4.2'
|
implementation 'com.github.moxy-community:moxy:2.1.2'
|
||||||
implementation 'androidx.room:room-ktx:2.4.2'
|
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
|
||||||
kapt 'androidx.room:room-compiler:2.4.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:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||||
implementation 'com.squareup.okio:okio:3.0.0'
|
implementation 'com.squareup.okio:okio:2.5.0'
|
||||||
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'org.koin:koin-android:2.1.5'
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
implementation 'io.coil-kt:coil:0.9.5'
|
||||||
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||||
|
implementation 'com.tomclaw.cache:cache:1.0'
|
||||||
|
|
||||||
implementation 'io.insert-koin:koin-android:3.1.6'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||||
implementation 'io.coil-kt:coil-base:1.4.0'
|
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.json:json:20190722'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
|
|
||||||
testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.4.2'
|
|
||||||
}
|
}
|
||||||
7
app/proguard-rules.pro
vendored
@@ -1,4 +1,3 @@
|
|||||||
-optimizationpasses 8
|
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
public static void checkExpressionValueIsNotNull(...);
|
public static void checkExpressionValueIsNotNull(...);
|
||||||
@@ -6,8 +5,8 @@
|
|||||||
public static void checkReturnedValueIsNotNull(...);
|
public static void checkReturnedValueIsNotNull(...);
|
||||||
public static void checkFieldIsNotNull(...);
|
public static void checkFieldIsNotNull(...);
|
||||||
public static void checkParameterIsNotNull(...);
|
public static void checkParameterIsNotNull(...);
|
||||||
public static void checkNotNullParameter(...);
|
|
||||||
}
|
}
|
||||||
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
|
|
||||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
|
||||||
|
public <init>(...);
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class MangaDatabaseTest {
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
|
||||||
MangaDatabase::class.java.canonicalName,
|
|
||||||
FrameworkSQLiteOpenHelperFactory()
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun migrateAll() {
|
|
||||||
helper.createDatabase(TEST_DB, 1).apply {
|
|
||||||
// TODO execSQL("")
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
for (migration in migrations) {
|
|
||||||
helper.runMigrationsAndValidate(
|
|
||||||
TEST_DB,
|
|
||||||
migration.endVersion,
|
|
||||||
true,
|
|
||||||
migration
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
const val TEST_DB = "test-db"
|
|
||||||
|
|
||||||
val migrations = arrayOf(
|
|
||||||
Migration1To2(),
|
|
||||||
Migration2To3(),
|
|
||||||
Migration3To4(),
|
|
||||||
Migration4To5(),
|
|
||||||
Migration5To6(),
|
|
||||||
Migration6To7(),
|
|
||||||
Migration7To8(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:tint="#E6000A">
|
||||||
|
<group android:scaleX="0.40188664"
|
||||||
|
android:scaleY="0.40188664"
|
||||||
|
android:translateX="32.90095"
|
||||||
|
android:translateY="18.7272">
|
||||||
|
<group android:translateY="139.39206">
|
||||||
|
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
|
||||||
|
android:fillColor="#E6000A"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
|
||||||
</resources>
|
|
||||||
4
app/src/debug/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">Kotatsu Dev</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,116 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="org.koitharu.kotatsu">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:dataExtractionRules="@xml/backup_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_content"
|
|
||||||
android:fullBackupOnly="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Kotatsu"
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
<activity android:name=".ui.main.MainActivity">
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="org.koitharu.kotatsu.ui.search.SearchActivity" />
|
android:value=".ui.search.SearchActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity android:name=".ui.details.MangaDetailsActivity">
|
||||||
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".ui.reader.ReaderActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
android:name=".ui.search.SearchActivity"
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="${applicationId}.action.READ_MANGA" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
|
||||||
android:label="@string/search_manga" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
android:name=".ui.settings.SettingsActivity"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
android:name=".ui.reader.SimpleSettingsActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:label="@string/settings">
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
|
||||||
android:label="@string/error_occurred"
|
|
||||||
android:theme="@android:style/Theme.DeviceDefault"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
|
||||||
android:label="@string/favourites_categories"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/manga_shelf">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".ui.browser.BrowserActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
android:name=".ui.utils.CrashActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/error_occurred"
|
||||||
|
android:theme="@android:style/Theme.DeviceDefault.Dialog"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
android:name=".ui.main.list.favourites.categories.CategoriesActivity"
|
||||||
android:noHistory="true"
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:label="@string/favourites_categories" />
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.settings.protect.ProtectSetupActivity"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:label="@string/downloads" />
|
|
||||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
android:name=".ui.download.DownloadService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name=".ui.settings.AppUpdateService" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
android:name=".ui.widget.shelf.ShelfWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
android:name=".ui.widget.recent.RecentWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
android:name=".ui.search.MangaSuggestionsProvider"
|
||||||
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<provider
|
<provider
|
||||||
@@ -122,37 +87,24 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/filepaths" />
|
android:resource="@xml/filepaths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver android:name=".ui.widget.shelf.ShelfWidgetProvider"
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/manga_shelf">
|
android:label="@string/manga_shelf">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data android:name="android.appwidget.provider"
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/widget_shelf" />
|
android:resource="@xml/widget_shelf" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver android:name=".ui.widget.recent.RecentWidgetProvider"
|
||||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/recent_manga">
|
android:label="@string/recent_manga">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data android:name="android.appwidget.provider"
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/widget_recent" />
|
android:resource="@xml/widget_recent" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
|
||||||
android:value="false" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
|
||||||
android:value="true" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,99 +1,125 @@
|
|||||||
package org.koitharu.kotatsu
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.StrictMode
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
import androidx.room.Room
|
||||||
import org.koin.android.ext.android.get
|
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.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koitharu.kotatsu.core.db.databaseModule
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.core.github.githubModule
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.networkModule
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.core.ui.uiModule
|
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.details.detailsModule
|
import org.koitharu.kotatsu.domain.history.HistoryRepository
|
||||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
|
||||||
import org.koitharu.kotatsu.history.historyModule
|
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.utils.CacheUtils
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import java.util.concurrent.TimeUnit
|
||||||
import org.koitharu.kotatsu.local.localModule
|
|
||||||
import org.koitharu.kotatsu.main.mainModule
|
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
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.suggestions.suggestionsModule
|
|
||||||
import org.koitharu.kotatsu.tracker.trackerModule
|
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
|
||||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
|
||||||
|
|
||||||
class KotatsuApp : Application() {
|
class KotatsuApp : Application() {
|
||||||
|
|
||||||
|
private val cookieJar by lazy {
|
||||||
|
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ChuckerCollector(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) {
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
enableStrictMode()
|
|
||||||
}
|
|
||||||
initKoin()
|
initKoin()
|
||||||
|
initCoil()
|
||||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
if (BuildConfig.DEBUG) {
|
||||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
initErrorHandler()
|
||||||
|
}
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
|
||||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||||
widgetUpdater.subscribeToFavourites(get())
|
FavouritesRepository.subscribe(widgetUpdater)
|
||||||
widgetUpdater.subscribeToHistory(get())
|
HistoryRepository.subscribe(widgetUpdater)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initKoin() {
|
private fun initKoin() {
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(this@KotatsuApp)
|
androidLogger()
|
||||||
|
androidContext(applicationContext)
|
||||||
modules(
|
modules(
|
||||||
networkModule,
|
module {
|
||||||
databaseModule,
|
factory {
|
||||||
githubModule,
|
okHttp()
|
||||||
uiModule,
|
.cache(CacheUtils.createHttpCache(applicationContext))
|
||||||
mainModule,
|
.build()
|
||||||
searchModule,
|
}
|
||||||
localModule,
|
single {
|
||||||
favouritesModule,
|
mangaDb().build()
|
||||||
historyModule,
|
}
|
||||||
remoteListModule,
|
single {
|
||||||
detailsModule,
|
MangaLoaderContext()
|
||||||
trackerModule,
|
}
|
||||||
settingsModule,
|
factory {
|
||||||
readerModule,
|
AppSettings(applicationContext)
|
||||||
appWidgetModule,
|
}
|
||||||
suggestionsModule,
|
single {
|
||||||
|
PagesCache(applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableStrictMode() {
|
private fun initCoil() {
|
||||||
StrictMode.setThreadPolicy(
|
Coil.setDefaultImageLoader(ImageLoader(applicationContext) {
|
||||||
StrictMode.ThreadPolicy.Builder()
|
okHttpClient {
|
||||||
.detectAll()
|
okHttp()
|
||||||
.penaltyLog()
|
.cache(CoilUtils.createDefaultCache(applicationContext))
|
||||||
.build()
|
.build()
|
||||||
)
|
}
|
||||||
StrictMode.setVmPolicy(
|
componentRegistry {
|
||||||
StrictMode.VmPolicy.Builder()
|
add(CbzFetcher())
|
||||||
.detectAll()
|
}
|
||||||
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
})
|
||||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
|
||||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
|
||||||
.penaltyLog()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
|
||||||
.penaltyDeath()
|
|
||||||
.detectFragmentReuse()
|
|
||||||
.detectWrongFragmentContainer()
|
|
||||||
.detectRetainInstanceUsage()
|
|
||||||
.detectSetUserVisibleHint()
|
|
||||||
.detectFragmentTagUsage()
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
|
|
||||||
class MangaDataRepository(private val db: MangaDatabase) {
|
|
||||||
|
|
||||||
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
|
|
||||||
val tags = manga.tags.toEntities()
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga.toEntity(), tags)
|
|
||||||
db.preferencesDao.upsert(
|
|
||||||
MangaPrefsEntity(
|
|
||||||
mangaId = manga.id,
|
|
||||||
mode = mode.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getReaderMode(mangaId: Long): ReaderMode? {
|
|
||||||
return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findMangaById(mangaId: Long): Manga? {
|
|
||||||
return db.mangaDao.find(mangaId)?.toManga()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
|
||||||
intent.manga != null -> intent.manga
|
|
||||||
intent.mangaId != 0L -> findMangaById(intent.mangaId)
|
|
||||||
else -> null // TODO resolve uri
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun storeManga(manga: Manga) {
|
|
||||||
val tags = manga.tags.toEntities()
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga.toEntity(), tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
|
||||||
return db.tagsDao.findTags(source.name).toMangaTags()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +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.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
class MangaIntent private constructor(
|
|
||||||
val manga: Manga?,
|
|
||||||
val mangaId: Long,
|
|
||||||
val uri: Uri?,
|
|
||||||
) {
|
|
||||||
|
|
||||||
constructor(intent: Intent?) : this(
|
|
||||||
manga = intent?.getParcelableExtra<ParcelableManga>(KEY_MANGA)?.manga,
|
|
||||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
|
||||||
uri = intent?.data
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(args: Bundle?) : this(
|
|
||||||
manga = args?.getParcelable<ParcelableManga>(KEY_MANGA)?.manga,
|
|
||||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
|
||||||
uri = null
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ID_NONE = 0L
|
|
||||||
|
|
||||||
const val KEY_MANGA = "manga"
|
|
||||||
const val KEY_ID = "id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Size
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
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.network.CommonHeaders
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.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
|
|
||||||
*/
|
|
||||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
|
|
||||||
try {
|
|
||||||
val page = pages.medianOrNull() ?: return null
|
|
||||||
val url = MangaRepository(page.source).getPageUrl(page)
|
|
||||||
val uri = Uri.parse(url)
|
|
||||||
val size = if (uri.scheme == "cbz") {
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
|
||||||
val entry = zip.getEntry(uri.fragment)
|
|
||||||
zip.getInputStream(entry).use {
|
|
||||||
getBitmapSize(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.get()
|
|
||||||
.header(CommonHeaders.REFERER, page.referer)
|
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
|
||||||
.build()
|
|
||||||
get<OkHttpClient>().newCall(request).await().use {
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
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)?.recycle()
|
|
||||||
val imageHeight: Int = options.outHeight
|
|
||||||
val imageWidth: Int = options.outWidth
|
|
||||||
check(imageHeight > 0 && imageWidth > 0)
|
|
||||||
return Size(imageWidth, imageHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +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.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 binding = onInflateView(layoutInflater, 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: MaterialAlertDialogBuilder) = Unit
|
|
||||||
|
|
||||||
protected fun bindingOrNull(): B? = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
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.annotation.CallSuper
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.appcompat.widget.ActionBarContextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
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.base.ui.util.ActionModeDelegate
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
|
|
||||||
abstract class BaseActivity<B : ViewBinding> :
|
|
||||||
AppCompatActivity(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
|
||||||
|
|
||||||
protected lateinit var binding: B
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
val actionModeDelegate = ActionModeDelegate()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
val settings = get<AppSettings>()
|
|
||||||
when {
|
|
||||||
settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
|
|
||||||
settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
|
|
||||||
}
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
insetsDelegate.handleImeInsets = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
val toolbar = (binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)
|
|
||||||
toolbar?.let(this::setSupportActionBar)
|
|
||||||
insetsDelegate.onViewCreated(binding.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupToolbar() {
|
|
||||||
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun isDarkAmoledTheme(): Boolean {
|
|
||||||
val uiMode = resources.configuration.uiMode
|
|
||||||
val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
||||||
return isNight && get<AppSettings>().isAmoledTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
|
||||||
super.onSupportActionModeStarted(mode)
|
|
||||||
actionModeDelegate.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
|
||||||
super.onSupportActionModeFinished(mode)
|
|
||||||
actionModeDelegate.onSupportActionModeFinished(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +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 android.view.ViewGroup.LayoutParams
|
|
||||||
import androidx.appcompat.app.AppCompatDialog
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
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)
|
|
||||||
|
|
||||||
protected val behavior: BottomSheetBehavior<*>?
|
|
||||||
get() = (dialog as? BottomSheetDialog)?.behavior
|
|
||||||
|
|
||||||
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, R.style.Theme_Kotatsu_Dialog)
|
|
||||||
} else super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
|
|
||||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
|
||||||
val b = behavior ?: return
|
|
||||||
if (isExpanded) {
|
|
||||||
b.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
b.isFitToContents = !isExpanded
|
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
|
||||||
rootView?.updateLayoutParams {
|
|
||||||
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
|
||||||
b.isDraggable = !isLocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
|
|
||||||
abstract class BaseFragment<B : ViewBinding> :
|
|
||||||
Fragment(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
protected val actionModeDelegate: ActionModeDelegate
|
|
||||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
|
||||||
|
|
||||||
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)
|
|
||||||
insetsDelegate.onViewCreated(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
insetsDelegate.onDestroyView()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun bindingOrNull() = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,53 +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
|
|
||||||
|
|
||||||
private 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
|
|
||||||
|
|
||||||
private 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
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
|
||||||
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
|
|
||||||
|
|
||||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
|
||||||
PreferenceFragmentCompat(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener,
|
|
||||||
RecyclerViewOwner {
|
|
||||||
|
|
||||||
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
override val recyclerView: RecyclerView
|
|
||||||
get() = listView
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
listView.clipToPadding = false
|
|
||||||
insetsDelegate.onViewCreated(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
insetsDelegate.onDestroyView()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
if (titleId != 0) {
|
|
||||||
setTitle(getString(titleId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
listView.updatePadding(
|
|
||||||
bottom = insets.bottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UsePropertyAccessSyntax")
|
|
||||||
protected fun setTitle(title: CharSequence) {
|
|
||||||
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
|
|
||||||
?: activity?.setTitle(title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
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 kotlinx.coroutines.runBlocking
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
|
|
||||||
DialogInterface by delegate {
|
|
||||||
|
|
||||||
fun show() = delegate.show()
|
|
||||||
|
|
||||||
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
|
|
||||||
|
|
||||||
private val adapter = VolumesAdapter(storageManager)
|
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (adapter.isEmpty) {
|
|
||||||
delegate.setMessage(R.string.cannot_find_available_storage)
|
|
||||||
} else {
|
|
||||||
val defaultValue = runBlocking {
|
|
||||||
storageManager.getDefaultWriteableDir()
|
|
||||||
}
|
|
||||||
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
|
|
||||||
it.first.canonicalPath == defaultValue?.canonicalPath
|
|
||||||
}
|
|
||||||
delegate.setAdapter(adapter) { 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(storageManager: LocalStorageManager) : BaseAdapter() {
|
|
||||||
|
|
||||||
var selectedItemPosition: Int = -1
|
|
||||||
val volumes = getAvailableVolumes(storageManager)
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
|
|
||||||
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
|
|
||||||
view.tag = it
|
|
||||||
}
|
|
||||||
val item = volumes[position]
|
|
||||||
binding.imageViewIndicator.isChecked = selectedItemPosition == position
|
|
||||||
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) = position.toLong()
|
|
||||||
|
|
||||||
override fun getCount() = volumes.size
|
|
||||||
|
|
||||||
override fun hasStableIds() = true
|
|
||||||
|
|
||||||
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
|
|
||||||
return runBlocking {
|
|
||||||
storageManager.getWriteableDirs().map {
|
|
||||||
it to storageManager.getStorageDisplayName(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnStorageSelectListener {
|
|
||||||
|
|
||||||
fun onStorageSelected(file: 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.inputEdit.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())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class FitHeightGridLayoutManager : GridLayoutManager {
|
|
||||||
|
|
||||||
constructor(context: Context?, spanCount: Int) : super(context, spanCount)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
context: Context?,
|
|
||||||
attrs: AttributeSet?,
|
|
||||||
defStyleAttr: Int,
|
|
||||||
defStyleRes: Int,
|
|
||||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
context: Context?,
|
|
||||||
spanCount: Int,
|
|
||||||
orientation: Int,
|
|
||||||
reverseLayout: Boolean,
|
|
||||||
) : super(context, spanCount, orientation, reverseLayout)
|
|
||||||
|
|
||||||
|
|
||||||
override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
|
||||||
val parentBottom = height - paddingBottom
|
|
||||||
val offset = parentBottom - bottom
|
|
||||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
|
||||||
} else {
|
|
||||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutParams
|
|
||||||
|
|
||||||
class FitHeightLinearLayoutManager : LinearLayoutManager {
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
|
||||||
constructor(
|
|
||||||
context: Context,
|
|
||||||
@RecyclerView.Orientation orientation: Int,
|
|
||||||
reverseLayout: Boolean,
|
|
||||||
) : super(context, orientation, reverseLayout)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet?,
|
|
||||||
@AttrRes defStyleAttr: Int,
|
|
||||||
@StyleRes defStyleRes: Int,
|
|
||||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
|
||||||
|
|
||||||
override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
|
|
||||||
val parentBottom = height - paddingBottom
|
|
||||||
val offset = parentBottom - bottom
|
|
||||||
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
|
|
||||||
} else {
|
|
||||||
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.content.res.getColorOrThrow
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val bounds = Rect()
|
|
||||||
private val thickness: Int
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
||||||
|
|
||||||
init {
|
|
||||||
paint.style = Paint.Style.FILL
|
|
||||||
val ta = context.obtainStyledAttributes(
|
|
||||||
null,
|
|
||||||
materialR.styleable.MaterialDivider,
|
|
||||||
materialR.attr.materialDividerStyle,
|
|
||||||
materialR.style.Widget_Material3_MaterialDivider,
|
|
||||||
)
|
|
||||||
paint.color = ta.getColorOrThrow(materialR.styleable.MaterialDivider_dividerColor)
|
|
||||||
thickness = ta.getDimensionPixelSize(
|
|
||||||
materialR.styleable.MaterialDivider_dividerThickness,
|
|
||||||
context.resources.getDimensionPixelSize(materialR.dimen.material_divider_thickness),
|
|
||||||
)
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State,
|
|
||||||
) {
|
|
||||||
outRect.set(0, thickness, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement for horizontal lists on demand
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
|
|
||||||
if (parent.layoutManager == null || thickness == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
canvas.save()
|
|
||||||
val left: Float
|
|
||||||
val right: Float
|
|
||||||
if (parent.clipToPadding) {
|
|
||||||
left = parent.paddingLeft.toFloat()
|
|
||||||
right = (parent.width - parent.paddingRight).toFloat()
|
|
||||||
canvas.clipRect(
|
|
||||||
left,
|
|
||||||
parent.paddingTop.toFloat(),
|
|
||||||
right,
|
|
||||||
(parent.height - parent.paddingBottom).toFloat()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
left = 0f
|
|
||||||
right = parent.width.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
var previous: RecyclerView.ViewHolder? = null
|
|
||||||
for (child in parent.children) {
|
|
||||||
val holder = parent.getChildViewHolder(child)
|
|
||||||
if (previous != null && shouldDrawDivider(previous, holder)) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
|
||||||
val top: Float = bounds.top + child.translationY
|
|
||||||
val bottom: Float = top + thickness
|
|
||||||
canvas.drawRect(left, top, right, bottom, paint)
|
|
||||||
}
|
|
||||||
previous = holder
|
|
||||||
}
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun shouldDrawDivider(
|
|
||||||
above: RecyclerView.ViewHolder,
|
|
||||||
below: RecyclerView.ViewHolder,
|
|
||||||
): Boolean
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.RectF
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
|
||||||
|
|
||||||
abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val bounds = Rect()
|
|
||||||
private val boundsF = RectF()
|
|
||||||
private val selection = HashSet<Long>()
|
|
||||||
|
|
||||||
protected var hasBackground: Boolean = true
|
|
||||||
protected var hasForeground: Boolean = false
|
|
||||||
protected var isIncludeDecorAndMargins: Boolean = true
|
|
||||||
|
|
||||||
val checkedItemsCount: Int
|
|
||||||
get() = selection.size
|
|
||||||
|
|
||||||
val checkedItemsIds: Set<Long>
|
|
||||||
get() = selection
|
|
||||||
|
|
||||||
fun toggleItemChecked(id: Long) {
|
|
||||||
if (!selection.remove(id)) {
|
|
||||||
selection.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItemIsChecked(id: Long, isChecked: Boolean) {
|
|
||||||
if (isChecked) {
|
|
||||||
selection.add(id)
|
|
||||||
} else {
|
|
||||||
selection.remove(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkAll(ids: Collection<Long>) {
|
|
||||||
selection.addAll(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSelection() {
|
|
||||||
selection.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
if (hasBackground) {
|
|
||||||
doDraw(canvas, parent, state, false)
|
|
||||||
} else {
|
|
||||||
super.onDraw(canvas, parent, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
if (hasForeground) {
|
|
||||||
doDraw(canvas, parent, state, true)
|
|
||||||
} else {
|
|
||||||
super.onDrawOver(canvas, parent, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State, isOver: Boolean) {
|
|
||||||
val checkpoint = canvas.save()
|
|
||||||
if (parent.clipToPadding) {
|
|
||||||
canvas.clipRect(
|
|
||||||
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
|
|
||||||
parent.height - parent.paddingBottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (child in parent.children) {
|
|
||||||
val itemId = getItemId(parent, child)
|
|
||||||
if (itemId != NO_ID && itemId in selection) {
|
|
||||||
if (isIncludeDecorAndMargins) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
|
||||||
} else {
|
|
||||||
bounds.set(child.left, child.top, child.right, child.bottom)
|
|
||||||
}
|
|
||||||
boundsF.set(bounds)
|
|
||||||
boundsF.offset(child.translationX, child.translationY)
|
|
||||||
if (isOver) {
|
|
||||||
onDrawForeground(canvas, parent, child, boundsF, state)
|
|
||||||
} else {
|
|
||||||
onDrawBackground(canvas, parent, child, boundsF, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvas.restoreToCount(checkpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
|
|
||||||
|
|
||||||
protected open fun onDrawBackground(
|
|
||||||
canvas: Canvas,
|
|
||||||
parent: RecyclerView,
|
|
||||||
child: View,
|
|
||||||
bounds: RectF,
|
|
||||||
state: RecyclerView.State,
|
|
||||||
) = Unit
|
|
||||||
|
|
||||||
protected open fun onDrawForeground(
|
|
||||||
canvas: Canvas,
|
|
||||||
parent: RecyclerView,
|
|
||||||
child: View,
|
|
||||||
bounds: RectF,
|
|
||||||
state: RecyclerView.State,
|
|
||||||
) = Unit
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
|
|
||||||
class ActionModeDelegate {
|
|
||||||
|
|
||||||
private var activeActionMode: ActionMode? = null
|
|
||||||
private var listeners: MutableList<ActionModeListener>? = null
|
|
||||||
|
|
||||||
val isActionModeStarted: Boolean
|
|
||||||
get() = activeActionMode != null
|
|
||||||
|
|
||||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
|
||||||
activeActionMode = mode
|
|
||||||
listeners?.forEach { it.onActionModeStarted(mode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSupportActionModeFinished(mode: ActionMode) {
|
|
||||||
activeActionMode = null
|
|
||||||
listeners?.forEach { it.onActionModeFinished(mode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addListener(listener: ActionModeListener) {
|
|
||||||
if (listeners == null) {
|
|
||||||
listeners = ArrayList()
|
|
||||||
}
|
|
||||||
checkNotNull(listeners).add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeListener(listener: ActionModeListener) {
|
|
||||||
listeners?.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addListener(listener: ActionModeListener, owner: LifecycleOwner) {
|
|
||||||
addListener(listener)
|
|
||||||
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ListenerLifecycleObserver(
|
|
||||||
private val listener: ActionModeListener,
|
|
||||||
) : DefaultLifecycleObserver {
|
|
||||||
|
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
|
||||||
super.onDestroy(owner)
|
|
||||||
removeListener(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
|
|
||||||
interface ActionModeListener {
|
|
||||||
|
|
||||||
fun onActionModeStarted(mode: ActionMode)
|
|
||||||
|
|
||||||
fun onActionModeFinished(mode: ActionMode)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
interface RecyclerViewOwner {
|
|
||||||
|
|
||||||
val recyclerView: RecyclerView
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
|
||||||
|
|
||||||
class ShrinkOnScrollBehavior : Behavior<ExtendedFloatingActionButton> {
|
|
||||||
|
|
||||||
@Suppress("unused") constructor() : super()
|
|
||||||
@Suppress("unused") constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
|
||||||
|
|
||||||
override fun onStartNestedScroll(
|
|
||||||
coordinatorLayout: CoordinatorLayout,
|
|
||||||
child: ExtendedFloatingActionButton,
|
|
||||||
directTargetChild: View,
|
|
||||||
target: View,
|
|
||||||
axes: Int,
|
|
||||||
type: Int
|
|
||||||
): Boolean {
|
|
||||||
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNestedScroll(
|
|
||||||
coordinatorLayout: CoordinatorLayout,
|
|
||||||
child: ExtendedFloatingActionButton,
|
|
||||||
target: View,
|
|
||||||
dxConsumed: Int,
|
|
||||||
dyConsumed: Int,
|
|
||||||
dxUnconsumed: Int,
|
|
||||||
dyUnconsumed: Int,
|
|
||||||
type: Int,
|
|
||||||
consumed: IntArray
|
|
||||||
) {
|
|
||||||
if (dyConsumed > 0) {
|
|
||||||
if (child.isExtended) {
|
|
||||||
child.shrink()
|
|
||||||
}
|
|
||||||
} else if (dyConsumed < 0) {
|
|
||||||
if (!child.isExtended) {
|
|
||||||
child.extend()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.OnApplyWindowInsetsListener
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
|
|
||||||
class WindowInsetsDelegate(
|
|
||||||
private val listener: WindowInsetsListener,
|
|
||||||
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
|
|
||||||
|
|
||||||
var handleImeInsets: Boolean = false
|
|
||||||
|
|
||||||
var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null
|
|
||||||
|
|
||||||
private var lastInsets: Insets? = null
|
|
||||||
|
|
||||||
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? {
|
|
||||||
if (insets == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets
|
|
||||||
val newInsets = if (handleImeInsets) {
|
|
||||||
Insets.max(
|
|
||||||
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars()),
|
|
||||||
handledInsets.getInsets(WindowInsetsCompat.Type.ime()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
}
|
|
||||||
if (newInsets != lastInsets) {
|
|
||||||
listener.onWindowInsetsChanged(newInsets)
|
|
||||||
lastInsets = newInsets
|
|
||||||
}
|
|
||||||
return handledInsets
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayoutChange(
|
|
||||||
view: View,
|
|
||||||
left: Int,
|
|
||||||
top: Int,
|
|
||||||
right: Int,
|
|
||||||
bottom: Int,
|
|
||||||
oldLeft: Int,
|
|
||||||
oldTop: Int,
|
|
||||||
oldRight: Int,
|
|
||||||
oldBottom: Int,
|
|
||||||
) {
|
|
||||||
view.removeOnLayoutChangeListener(this)
|
|
||||||
if (lastInsets == null) { // Listener may not be called
|
|
||||||
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onViewCreated(view: View) {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
|
||||||
view.addOnLayoutChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDestroyView() {
|
|
||||||
lastInsets = null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WindowInsetsListener {
|
|
||||||
|
|
||||||
fun onWindowInsetsChanged(insets: Insets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.core.view.children
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
|
|
||||||
class CheckableButtonGroup @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0,
|
|
||||||
) : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
|
||||||
|
|
||||||
var onCheckedChangeListener: OnCheckedChangeListener? = null
|
|
||||||
|
|
||||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
|
||||||
if (child is MaterialButton) {
|
|
||||||
child.setOnClickListener(this)
|
|
||||||
}
|
|
||||||
super.addView(child, index, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
setCheckedId(v.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCheckedId(@IdRes viewRes: Int) {
|
|
||||||
children.forEach {
|
|
||||||
(it as? MaterialButton)?.isChecked = it.id == viewRes
|
|
||||||
}
|
|
||||||
onCheckedChangeListener?.onCheckedChanged(this, viewRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnCheckedChangeListener {
|
|
||||||
fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import android.os.Parcelable.Creator
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.Checkable
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
|
||||||
import androidx.core.os.ParcelCompat
|
|
||||||
|
|
||||||
class CheckableImageView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0,
|
|
||||||
) : AppCompatImageView(context, attrs, defStyleAttr), Checkable {
|
|
||||||
|
|
||||||
private var isCheckedInternal = false
|
|
||||||
private var isBroadcasting = false
|
|
||||||
|
|
||||||
var onCheckedChangeListener: OnCheckedChangeListener? = null
|
|
||||||
|
|
||||||
override fun isChecked() = isCheckedInternal
|
|
||||||
|
|
||||||
override fun toggle() {
|
|
||||||
isChecked = !isCheckedInternal
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setChecked(checked: Boolean) {
|
|
||||||
if (checked != isCheckedInternal) {
|
|
||||||
isCheckedInternal = checked
|
|
||||||
refreshDrawableState()
|
|
||||||
if (!isBroadcasting) {
|
|
||||||
isBroadcasting = true
|
|
||||||
onCheckedChangeListener?.onCheckedChanged(this, checked)
|
|
||||||
isBroadcasting = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
|
||||||
val state = super.onCreateDrawableState(extraSpace + 1)
|
|
||||||
if (isCheckedInternal) {
|
|
||||||
mergeDrawableStates(state, intArrayOf(android.R.attr.state_checked))
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(): Parcelable? {
|
|
||||||
val superState = super.onSaveInstanceState() ?: return null
|
|
||||||
return SavedState(superState, isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
|
||||||
if (state is SavedState) {
|
|
||||||
super.onRestoreInstanceState(state.superState)
|
|
||||||
isChecked = state.isChecked
|
|
||||||
} else {
|
|
||||||
super.onRestoreInstanceState(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnCheckedChangeListener {
|
|
||||||
|
|
||||||
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SavedState : BaseSavedState {
|
|
||||||
|
|
||||||
val isChecked: Boolean
|
|
||||||
|
|
||||||
constructor(superState: Parcelable, checked: Boolean) : super(superState) {
|
|
||||||
isChecked = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: Parcel) : super(source) {
|
|
||||||
isChecked = ParcelCompat.readBoolean(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
|
||||||
super.writeToParcel(out, flags)
|
|
||||||
ParcelCompat.writeBoolean(out, isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField
|
|
||||||
val CREATOR: Creator<SavedState> = object : Creator<SavedState> {
|
|
||||||
override fun createFromParcel(`in`: Parcel) = SavedState(`in`)
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +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.chip.Chip
|
|
||||||
import com.google.android.material.chip.ChipDrawable
|
|
||||||
import com.google.android.material.chip.ChipGroup
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
class ChipsView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = com.google.android.material.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)
|
|
||||||
}
|
|
||||||
private var chipOnCloseListener = OnClickListener {
|
|
||||||
onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag)
|
|
||||||
}
|
|
||||||
var onChipClickListener: OnChipClickListener? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
val isChipClickable = value != null
|
|
||||||
children.forEach { it.isClickable = isChipClickable }
|
|
||||||
}
|
|
||||||
var onChipCloseClickListener: OnChipCloseClickListener? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
val isCloseIconVisible = value != null
|
|
||||||
children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requestLayout() {
|
|
||||||
if (isLayoutSuppressedCompat) {
|
|
||||||
isLayoutCalledOnSuppressed = true
|
|
||||||
} else {
|
|
||||||
super.requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChips(items: Collection<ChipModel>) {
|
|
||||||
suppressLayoutCompat(true)
|
|
||||||
try {
|
|
||||||
for ((i, model) in items.withIndex()) {
|
|
||||||
val chip = getChildAt(i) as Chip? ?: addChip()
|
|
||||||
bindChip(chip, model)
|
|
||||||
}
|
|
||||||
if (childCount > items.size) {
|
|
||||||
removeViews(items.size, childCount - items.size)
|
|
||||||
}
|
|
||||||
} 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.isClickable = onChipClickListener != null
|
|
||||||
chip.tag = model.data
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addChip(): Chip {
|
|
||||||
val chip = Chip(context)
|
|
||||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
|
|
||||||
chip.setChipDrawable(drawable)
|
|
||||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
|
||||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
|
||||||
chip.setEnsureMinTouchTargetSize(false)
|
|
||||||
chip.setOnClickListener(chipOnClickListener)
|
|
||||||
chip.isCheckable = false
|
|
||||||
addView(chip)
|
|
||||||
return chip
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun suppressLayoutCompat(suppress: Boolean) {
|
|
||||||
isLayoutSuppressedCompat = suppress
|
|
||||||
if (!suppress) {
|
|
||||||
if (isLayoutCalledOnSuppressed) {
|
|
||||||
requestLayout()
|
|
||||||
isLayoutCalledOnSuppressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChipModel(
|
|
||||||
@DrawableRes val icon: Int,
|
|
||||||
val title: CharSequence,
|
|
||||||
val data: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ChipModel
|
|
||||||
|
|
||||||
if (icon != other.icon) return false
|
|
||||||
if (title != other.title) return false
|
|
||||||
if (data != other.data) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = icon
|
|
||||||
result = 31 * result + title.hashCode()
|
|
||||||
result = 31 * result + data.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnChipClickListener {
|
|
||||||
|
|
||||||
fun onChipClick(chip: Chip, data: Any?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnChipCloseClickListener {
|
|
||||||
|
|
||||||
fun onChipCloseClick(chip: Chip, data: Any?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.LinearLayout.HORIZONTAL
|
|
||||||
import android.widget.LinearLayout.VERTICAL
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.core.content.withStyledAttributes
|
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
private const val ASPECT_RATIO_HEIGHT = 18f
|
|
||||||
private const val ASPECT_RATIO_WIDTH = 13f
|
|
||||||
|
|
||||||
class CoverImageView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0,
|
|
||||||
) : ShapeableImageView(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private var orientation: Int = HORIZONTAL
|
|
||||||
|
|
||||||
init {
|
|
||||||
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
|
|
||||||
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
val desiredWidth: Int
|
|
||||||
val desiredHeight: Int
|
|
||||||
if (orientation == VERTICAL) {
|
|
||||||
desiredHeight = measuredHeight
|
|
||||||
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
|
|
||||||
} else {
|
|
||||||
desiredWidth = measuredWidth
|
|
||||||
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
|
|
||||||
}
|
|
||||||
setMeasuredDimension(desiredWidth, desiredHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
private const val ENTER_DURATION = 300L
|
|
||||||
private const val EXIT_DURATION = 200L
|
|
||||||
private const val SHORT_DURATION = 1_500L
|
|
||||||
private const val LONG_DURATION = 2_750L
|
|
||||||
/**
|
|
||||||
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
|
|
||||||
*
|
|
||||||
* Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
|
|
||||||
*
|
|
||||||
* https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
|
|
||||||
*/
|
|
||||||
class FadingSnackbar @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private val message: TextView
|
|
||||||
private val action: Button
|
|
||||||
|
|
||||||
init {
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
|
|
||||||
message = view.findViewById(R.id.snackbar_text)
|
|
||||||
action = view.findViewById(R.id.snackbar_action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
if (visibility == VISIBLE && alpha == 1f) {
|
|
||||||
animate()
|
|
||||||
.alpha(0f)
|
|
||||||
.withEndAction { visibility = GONE }
|
|
||||||
.duration = EXIT_DURATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(
|
|
||||||
messageText: CharSequence? = null,
|
|
||||||
@StringRes actionId: Int? = null,
|
|
||||||
longDuration: Boolean = true,
|
|
||||||
actionClick: () -> Unit = { dismiss() },
|
|
||||||
dismissListener: () -> Unit = { }
|
|
||||||
) {
|
|
||||||
message.text = messageText
|
|
||||||
if (actionId != null) {
|
|
||||||
action.run {
|
|
||||||
visibility = VISIBLE
|
|
||||||
text = context.getString(actionId)
|
|
||||||
setOnClickListener {
|
|
||||||
actionClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
action.visibility = GONE
|
|
||||||
}
|
|
||||||
alpha = 0f
|
|
||||||
visibility = VISIBLE
|
|
||||||
animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.duration = ENTER_DURATION
|
|
||||||
val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
|
|
||||||
postDelayed(showDuration) {
|
|
||||||
dismiss()
|
|
||||||
dismissListener()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.content.res.TypedArray
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.graphics.drawable.InsetDrawable
|
|
||||||
import android.graphics.drawable.RippleDrawable
|
|
||||||
import android.graphics.drawable.ShapeDrawable
|
|
||||||
import android.graphics.drawable.shapes.RectShape
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.appcompat.widget.AppCompatCheckedTextView
|
|
||||||
import androidx.core.content.withStyledAttributes
|
|
||||||
import com.google.android.material.ripple.RippleUtils
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColorStateList
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
class ListItemTextView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = R.attr.listItemTextViewStyle,
|
|
||||||
) : AppCompatCheckedTextView(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private var checkedDrawableStart: Drawable? = null
|
|
||||||
private var checkedDrawableEnd: Drawable? = null
|
|
||||||
private var isInitialized = false
|
|
||||||
private var isCheckDrawablesVisible: Boolean = false
|
|
||||||
private var defaultPaddingStart: Int = 0
|
|
||||||
private var defaultPaddingEnd: Int = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
context.withStyledAttributes(attrs, R.styleable.ListItemTextView, defStyleAttr) {
|
|
||||||
val itemRippleColor = getColorStateList(R.styleable.ListItemTextView_rippleColor)
|
|
||||||
?: getRippleColorFallback(context)
|
|
||||||
val shape = createShapeDrawable(this)
|
|
||||||
background = RippleDrawable(
|
|
||||||
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor),
|
|
||||||
shape,
|
|
||||||
ShapeDrawable(RectShape()),
|
|
||||||
)
|
|
||||||
checkedDrawableStart = getDrawable(R.styleable.ListItemTextView_checkedDrawableStart)
|
|
||||||
checkedDrawableEnd = getDrawable(R.styleable.ListItemTextView_checkedDrawableEnd)
|
|
||||||
}
|
|
||||||
checkedDrawableStart?.setTintList(textColors)
|
|
||||||
checkedDrawableEnd?.setTintList(textColors)
|
|
||||||
defaultPaddingStart = paddingStart
|
|
||||||
defaultPaddingEnd = paddingEnd
|
|
||||||
isInitialized = true
|
|
||||||
adjustCheckDrawables()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun refreshDrawableState() {
|
|
||||||
super.refreshDrawableState()
|
|
||||||
adjustCheckDrawables()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setTextColor(colors: ColorStateList?) {
|
|
||||||
checkedDrawableStart?.setTintList(colors)
|
|
||||||
checkedDrawableEnd?.setTintList(colors)
|
|
||||||
super.setTextColor(colors)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
|
||||||
defaultPaddingStart = start
|
|
||||||
defaultPaddingEnd = end
|
|
||||||
super.setPaddingRelative(start, top, end, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
|
|
||||||
defaultPaddingStart = if (isRtl) right else left
|
|
||||||
defaultPaddingEnd = if (isRtl) left else right
|
|
||||||
super.setPadding(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustCheckDrawables() {
|
|
||||||
if (isInitialized && isCheckDrawablesVisible != isChecked) {
|
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
|
||||||
if (isChecked) checkedDrawableStart else null,
|
|
||||||
null,
|
|
||||||
if (isChecked) checkedDrawableEnd else null,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
super.setPaddingRelative(
|
|
||||||
if (isChecked && checkedDrawableStart != null) {
|
|
||||||
defaultPaddingStart + compoundDrawablePadding
|
|
||||||
} else defaultPaddingStart,
|
|
||||||
paddingTop,
|
|
||||||
if (isChecked && checkedDrawableEnd != null) {
|
|
||||||
defaultPaddingEnd + compoundDrawablePadding
|
|
||||||
} else defaultPaddingEnd,
|
|
||||||
paddingBottom,
|
|
||||||
)
|
|
||||||
isCheckDrawablesVisible = isChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createShapeDrawable(ta: TypedArray): InsetDrawable {
|
|
||||||
val shapeAppearance = ShapeAppearanceModel.builder(
|
|
||||||
context,
|
|
||||||
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearance, 0),
|
|
||||||
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearanceOverlay, 0),
|
|
||||||
).build()
|
|
||||||
val shapeDrawable = MaterialShapeDrawable(shapeAppearance)
|
|
||||||
shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundTint)
|
|
||||||
return InsetDrawable(
|
|
||||||
shapeDrawable,
|
|
||||||
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetLeft, 0),
|
|
||||||
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetTop, 0),
|
|
||||||
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetRight, 0),
|
|
||||||
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetBottom, 0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRippleColorFallback(context: Context): ColorStateList {
|
|
||||||
return context.getThemeColorStateList(android.R.attr.colorControlHighlight)
|
|
||||||
?: ColorStateList.valueOf(Color.TRANSPARENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
|
|
||||||
class WindowInsetHolder @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0,
|
|
||||||
) : View(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private var desiredHeight = 0
|
|
||||||
private var desiredWidth = 0
|
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
|
||||||
val barsInsets = WindowInsetsCompat.toWindowInsetsCompat(insets, this)
|
|
||||||
.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val gravity = getLayoutGravity()
|
|
||||||
val newWidth = when (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
|
|
||||||
Gravity.LEFT -> barsInsets.left
|
|
||||||
Gravity.RIGHT -> barsInsets.right
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
val newHeight = when (gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
|
||||||
Gravity.TOP -> barsInsets.top
|
|
||||||
Gravity.BOTTOM -> barsInsets.bottom
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
if (newWidth != desiredWidth || newHeight != desiredHeight) {
|
|
||||||
desiredWidth = newWidth
|
|
||||||
desiredHeight = newHeight
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
return super.dispatchApplyWindowInsets(insets)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
|
||||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
|
||||||
super.onMeasure(
|
|
||||||
if (desiredWidth == 0 || widthMode == MeasureSpec.EXACTLY) {
|
|
||||||
widthMeasureSpec
|
|
||||||
} else {
|
|
||||||
MeasureSpec.makeMeasureSpec(desiredWidth, widthMode)
|
|
||||||
},
|
|
||||||
if (desiredHeight == 0 || heightMode == MeasureSpec.EXACTLY) {
|
|
||||||
heightMeasureSpec
|
|
||||||
} else {
|
|
||||||
MeasureSpec.makeMeasureSpec(desiredHeight, heightMode)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLayoutGravity(): Int {
|
|
||||||
return when (val lp = layoutParams) {
|
|
||||||
is FrameLayout.LayoutParams -> lp.gravity
|
|
||||||
is LinearLayout.LayoutParams -> lp.gravity
|
|
||||||
is CoordinatorLayout.LayoutParams -> lp.gravity
|
|
||||||
else -> Gravity.NO_GRAVITY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
|
||||||
supportActionBar?.run {
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
|
||||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
|
||||||
}
|
|
||||||
with(binding.webView.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
}
|
|
||||||
binding.webView.webViewClient = BrowserClient(this)
|
|
||||||
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val url = intent?.dataString
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
finishAfterTransition()
|
|
||||||
} else {
|
|
||||||
onTitleChanged(
|
|
||||||
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
|
||||||
url
|
|
||||||
)
|
|
||||||
binding.webView.loadUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
binding.webView.saveState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
binding.webView.restoreState(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.opt_browser, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
android.R.id.home -> {
|
|
||||||
binding.webView.stopLoading()
|
|
||||||
finishAfterTransition()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_browser -> {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(binding.webView.url)
|
|
||||||
try {
|
|
||||||
startActivity(Intent.createChooser(intent, item.title))
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (binding.webView.canGoBack()) {
|
|
||||||
binding.webView.goBack()
|
|
||||||
} else {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
binding.webView.onPause()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
binding.webView.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
binding.webView.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
|
||||||
binding.progressBar.isVisible = isLoading
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
|
||||||
this.title = title
|
|
||||||
supportActionBar?.subtitle = subtitle
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.appbar.updatePadding(
|
|
||||||
top = insets.top,
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right,
|
|
||||||
)
|
|
||||||
binding.root.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right,
|
|
||||||
bottom = insets.bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val EXTRA_TITLE = "title"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, url: String, title: String?): Intent {
|
|
||||||
return Intent(context, BrowserActivity::class.java)
|
|
||||||
.setData(Uri.parse(url))
|
|
||||||
.putExtra(EXTRA_TITLE, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.webkit.WebView
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
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>(mode = LazyThreadSafetyMode.SYNCHRONIZED)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser
|
|
||||||
|
|
||||||
import android.webkit.WebChromeClient
|
|
||||||
import android.webkit.WebView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
|
||||||
|
|
||||||
private const val PROGRESS_MAX = 100
|
|
||||||
|
|
||||||
class ProgressChromeClient(
|
|
||||||
private val progressIndicator: BaseProgressIndicator<*>,
|
|
||||||
) : WebChromeClient() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
progressIndicator.max = PROGRESS_MAX
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
|
||||||
super.onProgressChanged(view, newProgress)
|
|
||||||
if (!progressIndicator.isVisible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (newProgress in 1 until PROGRESS_MAX) {
|
|
||||||
progressIndicator.isIndeterminate = false
|
|
||||||
progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true)
|
|
||||||
} else {
|
|
||||||
progressIndicator.setIndeterminate(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
interface CloudFlareCallback {
|
|
||||||
|
|
||||||
fun onPageLoaded()
|
|
||||||
|
|
||||||
fun onCheckPassed()
|
|
||||||
}
|
|
||||||
@@ -1,45 +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
|
|
||||||
|
|
||||||
private const val CF_CLEARANCE = "cf_clearance"
|
|
||||||
|
|
||||||
class CloudFlareClient(
|
|
||||||
private val cookieJar: AndroidCookieJar,
|
|
||||||
private val callback: CloudFlareCallback,
|
|
||||||
private val targetUrl: String
|
|
||||||
) : WebViewClientCompat() {
|
|
||||||
|
|
||||||
private val oldClearance = getCookieValue(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 clearance = getCookieValue(CF_CLEARANCE)
|
|
||||||
if (clearance != null && clearance != oldClearance) {
|
|
||||||
callback.onCheckPassed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCookieValue(name: String): String? {
|
|
||||||
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
|
||||||
.find { it.name == name }?.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.core.view.isInvisible
|
|
||||||
import androidx.fragment.app.setFragmentResult
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
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: MaterialAlertDialogBuilder) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
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
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
suspend fun createNew(context: Context): BackupArchive = runInterruptible(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).lowercase(Locale.ROOT))
|
|
||||||
append('_')
|
|
||||||
append(Date().format("ddMMyyyy"))
|
|
||||||
append(".bak")
|
|
||||||
}
|
|
||||||
BackupArchive(File(dir, filename))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
|
||||||
|
|
||||||
import org.json.JSONArray
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +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
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 10
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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("nsfw", isNsfw)
|
|
||||||
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)
|
|
||||||
jo.put("order", order)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +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.parsers.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
||||||
|
|
||||||
class RestoreRepository(private val db: MangaDatabase) {
|
|
||||||
|
|
||||||
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
|
|
||||||
val result = CompositeResult()
|
|
||||||
for (item in entry.data.JSONIterator()) {
|
|
||||||
val mangaJson = item.getJSONObject("manga")
|
|
||||||
val manga = parseManga(mangaJson)
|
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
|
||||||
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.JSONIterator()) {
|
|
||||||
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.JSONIterator()) {
|
|
||||||
val mangaJson = item.getJSONObject("manga")
|
|
||||||
val manga = parseManga(mangaJson)
|
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
|
||||||
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(),
|
|
||||||
isNsfw = json.getBooleanOrDefault("nsfw", false),
|
|
||||||
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"),
|
|
||||||
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
|
|
||||||
mangaId = json.getLong("manga_id"),
|
|
||||||
categoryId = json.getLong("category_id"),
|
|
||||||
createdAt = json.getLong("created_at")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val databaseModule
|
|
||||||
get() = module {
|
|
||||||
single { MangaDatabase.create(androidContext()) }
|
|
||||||
}
|
|
||||||
@@ -1,17 +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
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
|
|
||||||
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
|
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL(
|
|
||||||
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)",
|
|
||||||
arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FavouriteCategoriesDao {
|
||||||
|
|
||||||
|
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy")
|
||||||
|
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||||
|
|
||||||
|
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
|
||||||
|
abstract suspend fun delete(id: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
|
||||||
|
abstract suspend fun update(id: Long, title: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FavouritesDao {
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY :orderBy LIMIT :limit OFFSET :offset")
|
||||||
|
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
|
||||||
|
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
||||||
|
abstract suspend fun find(id: Long): FavouriteManga?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract suspend fun add(favourite: FavouriteEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||||
|
abstract suspend fun delete(categoryId: Long, mangaId: Long)
|
||||||
|
}
|
||||||
47
app/src/main/java/org/koitharu/kotatsu/core/db/HistoryDao.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class HistoryDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
|
||||||
|
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
|
||||||
|
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM history WHERE manga_id = :id")
|
||||||
|
abstract suspend fun find(id: Long): HistoryEntity?
|
||||||
|
|
||||||
|
@Query("DELETE FROM history")
|
||||||
|
abstract suspend fun clear()
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract suspend fun insert(entity: HistoryEntity): Long
|
||||||
|
|
||||||
|
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
|
||||||
|
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM history WHERE manga_id = :mangaId")
|
||||||
|
abstract suspend fun delete(mangaId: Long)
|
||||||
|
|
||||||
|
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open suspend fun upsert(entity: HistoryEntity): Boolean {
|
||||||
|
return if (update(entity) == 0) {
|
||||||
|
insert(entity)
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
@@ -13,14 +13,6 @@ abstract class MangaDao {
|
|||||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||||
abstract suspend fun find(id: Long): MangaWithTags?
|
abstract suspend fun find(id: Long): MangaWithTags?
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
|
|
||||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
|
|
||||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun insert(manga: MangaEntity): Long
|
abstract suspend fun insert(manga: MangaEntity): Long
|
||||||
|
|
||||||
@@ -1,28 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import org.koitharu.kotatsu.core.db.dao.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
import org.koitharu.kotatsu.core.db.entity.*
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
|
||||||
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
|
|
||||||
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
|
|
||||||
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
|
||||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
|
], version = 4
|
||||||
],
|
|
||||||
version = 9
|
|
||||||
)
|
)
|
||||||
abstract class MangaDatabase : RoomDatabase() {
|
abstract class MangaDatabase : RoomDatabase() {
|
||||||
|
|
||||||
@@ -39,28 +25,4 @@ abstract class MangaDatabase : RoomDatabase() {
|
|||||||
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
||||||
|
|
||||||
abstract val tracksDao: TracksDao
|
abstract val tracksDao: TracksDao
|
||||||
|
|
||||||
abstract val trackLogsDao: TrackLogsDao
|
|
||||||
|
|
||||||
abstract val suggestionDao: SuggestionDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(context: Context): MangaDatabase = Room.databaseBuilder(
|
|
||||||
context,
|
|
||||||
MangaDatabase::class.java,
|
|
||||||
"kotatsu-db"
|
|
||||||
).addMigrations(
|
|
||||||
Migration1To2(),
|
|
||||||
Migration2To3(),
|
|
||||||
Migration3To4(),
|
|
||||||
Migration4To5(),
|
|
||||||
Migration5To6(),
|
|
||||||
Migration6To7(),
|
|
||||||
Migration7To8(),
|
|
||||||
Migration8To9(),
|
|
||||||
).addCallback(
|
|
||||||
DatabasePrePopulateCallback(context.resources)
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
26
app/src/main/java/org/koitharu/kotatsu/core/db/TagsDao.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TagsDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tags")
|
||||||
|
suspend fun getAllTags(): List<TagEntity>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insert(tag: TagEntity): Long
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun update(tag: TagEntity): Int
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsert(tags: Iterable<TagEntity>) {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
if (update(tag) <= 0) {
|
||||||
|
insert(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
||||||
@@ -13,9 +13,6 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
|
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
|
||||||
abstract suspend fun find(mangaId: Long): TrackEntity?
|
abstract suspend fun find(mangaId: Long): TrackEntity?
|
||||||
|
|
||||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun findNewChapters(mangaId: Long): Int?
|
|
||||||
|
|
||||||
@Query("DELETE FROM tracks")
|
@Query("DELETE FROM tracks")
|
||||||
abstract suspend fun clear()
|
abstract suspend fun clear()
|
||||||
|
|
||||||
@@ -28,13 +25,11 @@ abstract class TracksDao {
|
|||||||
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
|
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
|
||||||
abstract suspend fun delete(mangaId: Long)
|
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
|
@Transaction
|
||||||
open suspend fun upsert(entity: TrackEntity) {
|
open suspend fun upsert(entity: TrackEntity) {
|
||||||
if (update(entity) == 0) {
|
if (update(entity) == 0) {
|
||||||
insert(entity)
|
insert(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class TagsDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tags WHERE source = :source")
|
|
||||||
abstract suspend fun findTags(source: String): List<TagEntity>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""SELECT tags.* FROM tags
|
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
|
||||||
GROUP BY tags.title
|
|
||||||
ORDER BY COUNT(manga_id) DESC
|
|
||||||
LIMIT :limit"""
|
|
||||||
)
|
|
||||||
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""SELECT tags.* FROM tags
|
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
|
||||||
WHERE tags.source = :source
|
|
||||||
GROUP BY tags.title
|
|
||||||
ORDER BY COUNT(manga_id) DESC
|
|
||||||
LIMIT :limit"""
|
|
||||||
)
|
|
||||||
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""SELECT tags.* FROM tags
|
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
|
||||||
WHERE tags.source = :source AND title LIKE :query
|
|
||||||
GROUP BY tags.title
|
|
||||||
ORDER BY COUNT(manga_id) DESC
|
|
||||||
LIMIT :limit"""
|
|
||||||
)
|
|
||||||
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""SELECT tags.* FROM tags
|
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
|
||||||
WHERE title LIKE :query
|
|
||||||
GROUP BY tags.title
|
|
||||||
ORDER BY COUNT(manga_id) DESC
|
|
||||||
LIMIT :limit"""
|
|
||||||
)
|
|
||||||
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun insert(tag: TagEntity): Long
|
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun update(tag: TagEntity): Int
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(tags: Iterable<TagEntity>) {
|
|
||||||
tags.forEach { tag ->
|
|
||||||
if (update(tag) <= 0) {
|
|
||||||
insert(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
import org.koitharu.kotatsu.parsers.util.longHashCode
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
||||||
|
|
||||||
// Entity to model
|
|
||||||
|
|
||||||
fun TagEntity.toMangaTag() = MangaTag(
|
|
||||||
key = this.key,
|
|
||||||
title = this.title.toTitleCase(),
|
|
||||||
source = MangaSource.valueOf(this.source),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
|
||||||
|
|
||||||
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
|
||||||
id = this.id,
|
|
||||||
title = this.title,
|
|
||||||
altTitle = this.altTitle,
|
|
||||||
state = this.state?.let { MangaState.valueOf(it) },
|
|
||||||
rating = this.rating,
|
|
||||||
isNsfw = this.isNsfw,
|
|
||||||
url = this.url,
|
|
||||||
publicUrl = this.publicUrl,
|
|
||||||
coverUrl = this.coverUrl,
|
|
||||||
largeCoverUrl = this.largeCoverUrl,
|
|
||||||
author = this.author,
|
|
||||||
source = MangaSource.valueOf(this.source),
|
|
||||||
tags = tags
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
|
||||||
|
|
||||||
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
|
|
||||||
id = trackLog.id,
|
|
||||||
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
|
||||||
manga = manga.toManga(tags.toMangaTags()),
|
|
||||||
createdAt = Date(trackLog.createdAt)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Model to entity
|
|
||||||
|
|
||||||
fun Manga.toEntity() = MangaEntity(
|
|
||||||
id = id,
|
|
||||||
url = url,
|
|
||||||
publicUrl = publicUrl,
|
|
||||||
source = source.name,
|
|
||||||
largeCoverUrl = largeCoverUrl,
|
|
||||||
coverUrl = coverUrl,
|
|
||||||
altTitle = altTitle,
|
|
||||||
rating = rating,
|
|
||||||
isNsfw = isNsfw,
|
|
||||||
state = state?.name,
|
|
||||||
title = title,
|
|
||||||
author = author,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaTag.toEntity() = TagEntity(
|
|
||||||
title = title,
|
|
||||||
key = key,
|
|
||||||
source = source.name,
|
|
||||||
id = "${key}_${source.name}".longHashCode()
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
|
|
||||||
|
|
||||||
// Other
|
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
|
|
||||||
SortOrder.valueOf(name)
|
|
||||||
}.getOrDefault(fallback)
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity(tableName = "favourite_categories")
|
||||||
|
data class FavouriteCategoryEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = "category_id") val categoryId: Int,
|
||||||
|
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||||
|
@ColumnInfo(name = "title") val title: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
|
||||||
|
id = id ?: categoryId.toLong(),
|
||||||
|
title = title,
|
||||||
|
createdAt = Date(createdAt)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.favourites.data
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||||
@@ -21,7 +20,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class FavouriteEntity(
|
data class FavouriteEntity(
|
||||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||||
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
||||||
@ColumnInfo(name = "created_at") val createdAt: Long
|
@ColumnInfo(name = "created_at") val createdAt: Long
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.favourites.data
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
|
||||||
|
|
||||||
class FavouriteManga(
|
data class FavouriteManga(
|
||||||
@Embedded val favourite: FavouriteEntity,
|
@Embedded val favourite: FavouriteEntity,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "manga_id",
|
parentColumn = "manga_id",
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.history.data
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "history",
|
tableName = "history", foreignKeys = [
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
entity = MangaEntity::class,
|
entity = MangaEntity::class,
|
||||||
parentColumns = ["manga_id"],
|
parentColumns = ["manga_id"],
|
||||||
@@ -17,12 +17,21 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class HistoryEntity(
|
data class HistoryEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||||
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
||||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||||
@ColumnInfo(name = "page") val page: Int,
|
@ColumnInfo(name = "page") val page: Int,
|
||||||
@ColumnInfo(name = "scroll") val scroll: Float,
|
@ColumnInfo(name = "scroll") val scroll: Float
|
||||||
)
|
) {
|
||||||
|
|
||||||
|
fun toMangaHistory() = MangaHistory(
|
||||||
|
createdAt = Date(createdAt),
|
||||||
|
updatedAt = Date(updatedAt),
|
||||||
|
chapterId = chapterId,
|
||||||
|
page = page,
|
||||||
|
scroll = scroll
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import androidx.room.Embedded
|
|||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
|
||||||
class TrackLogWithManga(
|
data class HistoryWithManga(
|
||||||
@Embedded val trackLog: TrackLogEntity,
|
@Embedded val history: HistoryEntity,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "manga_id",
|
parentColumn = "manga_id",
|
||||||
entityColumn = "manga_id"
|
entityColumn = "manga_id"
|
||||||
@@ -16,5 +16,5 @@ class TrackLogWithManga(
|
|||||||
entityColumn = "tag_id",
|
entityColumn = "tag_id",
|
||||||
associateBy = Junction(MangaTagsEntity::class)
|
associateBy = Junction(MangaTagsEntity::class)
|
||||||
)
|
)
|
||||||
val tags: List<TagEntity>,
|
val tags: List<TagEntity>
|
||||||
)
|
)
|
||||||
@@ -3,20 +3,53 @@ package org.koitharu.kotatsu.core.db.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
|
||||||
@Entity(tableName = "manga")
|
@Entity(tableName = "manga")
|
||||||
class MangaEntity(
|
data class MangaEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
@ColumnInfo(name = "manga_id") val id: Long,
|
@ColumnInfo(name = "manga_id") val id: Long,
|
||||||
@ColumnInfo(name = "title") val title: String,
|
@ColumnInfo(name = "title") val title: String,
|
||||||
@ColumnInfo(name = "alt_title") val altTitle: String?,
|
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
|
||||||
@ColumnInfo(name = "url") val url: String,
|
@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 = "rating") val rating: Float, // normalized value [0..1] or -1
|
|
||||||
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
|
|
||||||
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
||||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
|
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
|
||||||
@ColumnInfo(name = "state") val state: String?,
|
@ColumnInfo(name = "state") val state: String? = null,
|
||||||
@ColumnInfo(name = "author") val author: String?,
|
@ColumnInfo(name = "author") val author: String? = null,
|
||||||
@ColumnInfo(name = "source") val source: String
|
@ColumnInfo(name = "source") val source: String
|
||||||
)
|
) {
|
||||||
|
|
||||||
|
fun toManga(tags: Set<MangaTag> = emptySet()) = Manga(
|
||||||
|
id = this.id,
|
||||||
|
title = this.title,
|
||||||
|
altTitle = this.altTitle,
|
||||||
|
state = this.state?.let { MangaState.valueOf(it) },
|
||||||
|
rating = this.rating,
|
||||||
|
url = this.url,
|
||||||
|
coverUrl = this.coverUrl,
|
||||||
|
largeCoverUrl = this.largeCoverUrl,
|
||||||
|
author = this.author,
|
||||||
|
source = MangaSource.valueOf(this.source),
|
||||||
|
tags = tags
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun from(manga: Manga) = MangaEntity(
|
||||||
|
id = manga.id,
|
||||||
|
url = manga.url,
|
||||||
|
source = manga.source.name,
|
||||||
|
largeCoverUrl = manga.largeCoverUrl,
|
||||||
|
coverUrl = manga.coverUrl,
|
||||||
|
altTitle = manga.altTitle,
|
||||||
|
rating = manga.rating,
|
||||||
|
state = manga.state?.name,
|
||||||
|
title = manga.title,
|
||||||
|
author = manga.author
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||