Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0153e90bf0 | ||
|
|
d4f8fe83f5 | ||
|
|
d28b1e4094 | ||
|
|
cd2de0136a |
BIN
.github/assets/vtuber.png
vendored
BIN
.github/assets/vtuber.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
114
.github/workflows/auto_release.yml
vendored
Normal file
114
.github/workflows/auto_release.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Build automatic release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
should_build: ${{ steps.check-updates.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Check for updates 🌏
|
||||
id: check-updates
|
||||
run: |
|
||||
last_run=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.created_at')
|
||||
kotatsu_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/Kotatsu/commits?since=$last_run" | jq '. | length')
|
||||
parsers_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/kotatsu-parsers/commits?since=$last_run" | jq '. | length')
|
||||
if [ "$kotatsu_updated" -gt "0" ] || [ "$parsers_updated" -gt "0" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: check
|
||||
if: needs.check.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
new_tag: ${{ steps.tagger.outputs.new_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: autobuild
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Android SDK 💻
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Grant permissions 💻
|
||||
run: chmod a+x gradlew
|
||||
|
||||
- name: Generate build number 📆
|
||||
id: tagger
|
||||
run: |
|
||||
echo "new_tag=$(./gradlew -q versionInfo -DbuildNumberIncrement=true)" >> $GITHUB_OUTPUT
|
||||
echo "formatted_date=$(date +'%Y/%m/%d')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decode Keystore
|
||||
id: decode_keystore
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: 'keystore/kotatsu.jks'
|
||||
encodedString: ${{ secrets.ANDROID_SIGNING_KEY }}
|
||||
|
||||
- name: Building new APK 💻
|
||||
run: >-
|
||||
./gradlew assembleRelease
|
||||
-DparsersVersionOverride=$(curl -s https://api.github.com/repos/kotatsuapp/kotatsu-parsers/commits/master -H "Accept: application/vnd.github.sha" | cut -c -10)
|
||||
|
||||
- name: Prepare to Upload 🌏
|
||||
run: |
|
||||
mv ${{steps.sign_app.outputs.signedFile}} app/build/outputs/apk/release/release.apk
|
||||
echo "SIGNED_APK=app/build/outputs/apk/release/release.apk" >> $GITHUB_ENV
|
||||
|
||||
- name: Get latest changes 📑
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG=$(cat CHANGELOG.txt)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGELOG" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create new GH Release + Uploading 🌏
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.tagger.outputs.new_tag }}
|
||||
name: "Build ${{ steps.tagger.outputs.new_tag }}"
|
||||
body: |
|
||||
Automated build generated on ${{ steps.tagger.outputs.formatted_date }}
|
||||
|
||||
${{ steps.changelog.outputs.content }}
|
||||
files: ${{ env.SIGNED_APK }}
|
||||
prerelease: false
|
||||
|
||||
update:
|
||||
needs: build
|
||||
if: needs.check.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: autobuild
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Commit 🌏
|
||||
run: |
|
||||
git config --local user.email "autorelease@users.noreply.github.com"
|
||||
git config --local user.name "autorelease"
|
||||
if [[ -n $(git status -s) ]]; then
|
||||
git add README.md
|
||||
git commit -m "Automatic release v${{ needs.build.outputs.new_tag }}"
|
||||
git push origin autobuild
|
||||
else
|
||||
echo "No changes to push!"
|
||||
fi
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,4 +26,4 @@
|
||||
.cxx
|
||||
/.idea/deviceManager.xml
|
||||
/.kotlin/
|
||||
/.idea/AndroidProjectSystem.xml
|
||||
/.idea/AndroidProjectSystem.xml
|
||||
|
||||
6
.idea/AndroidProjectSystem.xml
generated
6
.idea/AndroidProjectSystem.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
@@ -6,13 +6,14 @@
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-21" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
106
README.md
106
README.md
@@ -1,107 +1,57 @@
|
||||
<div align="center">
|
||||
# Kotatsu
|
||||
|
||||
<a href="https://kotatsu.app">
|
||||
<img src="./.github/assets/vtuber.png" alt="Kotatsu Logo" title="Kotatsu" width="600"/>
|
||||
</a>
|
||||
Kotatsu is a free and open-source manga reader for Android with built-in online content sources.
|
||||
|
||||
# [Kotatsu](https://kotatsu.app)
|
||||
|
||||
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in online content sources.**
|
||||
|
||||
   [](https://github.com/KotatsuApp/kotatsu-parsers) [](https://hosted.weblate.org/engage/kotatsu/) [](https://discord.gg/NNJ5RgVBC5) [](https://t.me/kotatsuapp) [](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
|
||||
[](https://github.com/KotatsuApp/kotatsu-parsers)   [](https://hosted.weblate.org/engage/kotatsu/) [](https://t.me/kotatsuapp) [](https://discord.gg/NNJ5RgVBC5) [](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
|
||||
|
||||
### Download
|
||||
|
||||
<div align="left">
|
||||
|
||||
* **Recommended:** Download and install APK from [GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest). Application has a built-in self-updating feature.
|
||||
* Get it on [F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu). The F-Droid build may be a bit outdated and some fixes might be missing.
|
||||
* Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
|
||||
|
||||
</div>
|
||||
- **Recommended:** Download and install APK from **[GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. Application has a built-in self-updating feature.
|
||||
- Get it on **[F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu)**. The F-Droid build may be a bit outdated and some fixes might be missing.
|
||||
- Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
|
||||
|
||||
### Main Features
|
||||
|
||||
<div align="left">
|
||||
|
||||
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers) (with 1100+ manga sources)
|
||||
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
|
||||
* Search manga by name, genres, and more filters
|
||||
* Reading history and bookmarks
|
||||
* Favorites organized by user-defined categories
|
||||
* Reading history, bookmarks, and incognito mode support
|
||||
* Download manga and read it offline. Third-party CBZ archives are also supported
|
||||
* Clean and convenient Material You UI, optimized for phones, tablets, and desktop
|
||||
* Standard and Webtoon-optimized customizable reader, gesture support on reading interface
|
||||
* Notifications about new chapters with updates feed, manga recommendations (with filters)
|
||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
||||
* Tablet-optimized Material You UI
|
||||
* Standard and Webtoon-optimized customizable reader
|
||||
* Notifications about new chapters with updates feed
|
||||
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
|
||||
* Password / fingerprint-protected access to the app
|
||||
* Automatically sync app data with other devices on the same account
|
||||
* Support for older devices running Android 5+
|
||||
* Password/fingerprint-protected access to the app
|
||||
|
||||
</div>
|
||||
### Screenshots
|
||||
|
||||
### In-App Screenshots
|
||||
|  |  |  |
|
||||
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
|
||||
|  |  |  |
|
||||
|
||||
<div align="center">
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/1.png" alt="Mobile view" width="250"/>
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/2.png" alt="Mobile view" width="250"/>
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/3.png" alt="Mobile view" width="250"/>
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/4.png" alt="Mobile view" width="250"/>
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/5.png" alt="Mobile view" width="250"/>
|
||||
<img src="./metadata/en-US/images/phoneScreenshots/6.png" alt="Mobile view" width="250"/>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
<img src="./metadata/en-US/images/tenInchScreenshots/1.png" alt="Tablet view" width="400"/>
|
||||
<img src="./metadata/en-US/images/tenInchScreenshots/2.png" alt="Tablet view" width="400"/>
|
||||
</div>
|
||||
|  |  |
|
||||
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||
|
||||
### Localization
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/kotatsu/">
|
||||
<img src="https://hosted.weblate.org/widget/kotatsu/horizontal-auto.png" alt="Translation status" />
|
||||
</a>
|
||||
[<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
|
||||
|
||||
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is localized in a number of different languages.**<br>
|
||||
**📌 If you would like to help improve these or add new languages,
|
||||
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)**
|
||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
||||
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
||||
|
||||
### Contributing
|
||||
|
||||
<br>
|
||||
|
||||
<a href="https://github.com/KotatsuApp/Kotatsu">
|
||||
<picture>
|
||||
<source srcset="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu&bg_color=0d1117&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" alt="Kotatsu GitHub Repository">
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/KotatsuApp/Kotatsu-parsers">
|
||||
<picture>
|
||||
<source srcset="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu-parsers&bg_color=0d1117&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu-parsers&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" alt="Kotatsu-parsers GitHub Repository">
|
||||
</picture>
|
||||
</a><br></br>
|
||||
|
||||
</br>
|
||||
|
||||
**📌 Pull requests are welcome, if you want: See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines**
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||
|
||||
### License
|
||||
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
<div align="left">
|
||||
|
||||
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
|
||||
|
||||
</div>
|
||||
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
|
||||
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
|
||||
install instructions.
|
||||
|
||||
### DMCA disclaimer
|
||||
|
||||
<div align="left">
|
||||
|
||||
The developers of this application do not have any affiliation with the content available in the app. It collects content from sources that are freely available through any web browser.
|
||||
|
||||
</div>
|
||||
The developers of this application do not have any affiliation with the content available in the app.
|
||||
It collects content from sources that are freely available through any web browser
|
||||
|
||||
@@ -7,29 +7,52 @@ plugins {
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'kotlin-parcelize'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
id 'androidx.room'
|
||||
}
|
||||
|
||||
def Properties versionProps = getVersionProps()
|
||||
|
||||
android {
|
||||
compileSdk = 35
|
||||
buildToolsVersion = '35.0.0'
|
||||
namespace = 'org.koitharu.kotatsu'
|
||||
|
||||
defaultConfig {
|
||||
def code = versionProps['code'].toInteger()
|
||||
def base = versionProps['base'].trim()
|
||||
def build = versionProps['build'].toInteger()
|
||||
def variant = versionProps['variant'].trim()
|
||||
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 1003
|
||||
versionName = '8.0-b3'
|
||||
versionCode = code * 1000 + build
|
||||
versionName = base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
arg('room.generateKotlin', 'true')
|
||||
arg('room.schemaLocation', "$projectDir/schemas")
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||
def allFilesFromDir = new File(tmpFilePath).listFiles()
|
||||
|
||||
if (allFilesFromDir != null) {
|
||||
def keystoreFile = allFilesFromDir.first()
|
||||
keystoreFile.renameTo("keystore/kotatsu.jks")
|
||||
}
|
||||
|
||||
storeFile = file("keystore/kotatsu.jks")
|
||||
storePassword System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = '.debug'
|
||||
@@ -38,6 +61,7 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
nightly {
|
||||
initWith release
|
||||
@@ -74,12 +98,8 @@ android {
|
||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||
'-opt-in=coil3.annotation.ExperimentalCoilApi',
|
||||
'-opt-in=coil3.annotation.InternalCoilApi',
|
||||
]
|
||||
}
|
||||
room {
|
||||
schemaDirectory "$projectDir/schemas"
|
||||
}
|
||||
lint {
|
||||
abortOnError true
|
||||
disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled', 'SimpleDateFormat'
|
||||
@@ -198,3 +218,22 @@ dependencies {
|
||||
androidTestImplementation libs.hilt.android.testing
|
||||
kaptAndroidTest libs.hilt.android.compiler
|
||||
}
|
||||
|
||||
tasks.register('versionInfo') {
|
||||
def base = versionProps['base'].trim()
|
||||
def build = versionProps['build'].toInteger()
|
||||
def variant = versionProps['variant'].trim()
|
||||
println base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
|
||||
}
|
||||
|
||||
def getVersionProps() {
|
||||
def versionPropsFile = file('version.properties')
|
||||
def Properties versionProps = new Properties()
|
||||
versionProps.load(new FileInputStream(versionPropsFile))
|
||||
if (System.getProperty('buildNumberIncrement') == 'true') {
|
||||
def code = versionProps['build'].toInteger() + 1
|
||||
versionProps['build'] = code.toString()
|
||||
versionProps.store(versionPropsFile.newWriter(), null)
|
||||
}
|
||||
return versionProps
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.koitharu.kotatsu
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.StrictMode
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||
import leakcanary.LeakCanary
|
||||
import org.koitharu.kotatsu.core.BaseApp
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
@@ -16,23 +13,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
class KotatsuApp : BaseApp() {
|
||||
|
||||
var isLeakCanaryEnabled: Boolean
|
||||
get() = getDebugPreferences(this).getBoolean(KEY_LEAK_CANARY, true)
|
||||
set(value) {
|
||||
getDebugPreferences(this).edit { putBoolean(KEY_LEAK_CANARY, value) }
|
||||
configureLeakCanary()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
enableStrictMode()
|
||||
configureLeakCanary()
|
||||
}
|
||||
|
||||
private fun configureLeakCanary() {
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
dumpHeap = isLeakCanaryEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
private fun enableStrictMode() {
|
||||
@@ -72,7 +55,7 @@ class KotatsuApp : BaseApp() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
|
||||
penaltyListener(notifier.executor, notifier)
|
||||
}
|
||||
}.build(),
|
||||
}.build()
|
||||
)
|
||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply {
|
||||
detectWrongFragmentContainer()
|
||||
@@ -87,13 +70,4 @@ class KotatsuApp : BaseApp() {
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val PREFS_DEBUG = "_debug"
|
||||
const val KEY_LEAK_CANARY = "leak_canary"
|
||||
|
||||
fun getDebugPreferences(context: Context): SharedPreferences =
|
||||
context.getSharedPreferences(PREFS_DEBUG, MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class StrictModeNotifier(
|
||||
.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
context,
|
||||
violation.hashCode(),
|
||||
0,
|
||||
ShareHelper(context).getShareTextIntent(violation.stackTraceToString()),
|
||||
0,
|
||||
false,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import leakcanary.AppWatcher
|
||||
|
||||
abstract class BaseService : LifecycleService() {
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(ContextCompat.getContextForLanguage(newBase))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
AppWatcher.objectWatcher.watch(
|
||||
watchedObject = this,
|
||||
description = "${javaClass.simpleName} service received Service#onDestroy() callback",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import leakcanary.LeakCanary
|
||||
import org.koitharu.kotatsu.KotatsuApp
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.workinspector.WorkInspector
|
||||
|
||||
@@ -14,18 +13,10 @@ class SettingsMenuProvider(
|
||||
private val context: Context,
|
||||
) : MenuProvider {
|
||||
|
||||
private val application: KotatsuApp
|
||||
get() = context.applicationContext as KotatsuApp
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_settings, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
menu.findItem(R.id.action_leakcanary).isChecked = application.isLeakCanaryEnabled
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_leaks -> {
|
||||
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
|
||||
@@ -37,13 +28,6 @@ class SettingsMenuProvider(
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_leakcanary -> {
|
||||
val checked = !menuItem.isChecked
|
||||
menuItem.isChecked = checked
|
||||
application.isLeakCanaryEnabled = checked
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_leakcanary"
|
||||
android:checkable="true"
|
||||
android:title="LeakCanary"
|
||||
app:showAsAction="never"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_leaks"
|
||||
android:id="@id/action_leaks"
|
||||
android:title="@string/leak_canary_display_activity_label"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_works"
|
||||
android:id="@id/action_works"
|
||||
android:title="@string/wi_lib_name"
|
||||
app:showAsAction="never" />
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:fullBackupOnly="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
@@ -281,10 +280,6 @@
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
|
||||
android:label="@string/local_manga_processing" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.settings.backup.RestoreService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/restore_backup" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
|
||||
@@ -3,28 +3,32 @@ package org.koitharu.kotatsu.alternatives.domain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.almostEquals
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
import org.koitharu.kotatsu.search.domain.SearchV2Helper
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MAX_PARALLELISM = 4
|
||||
private const val MATCH_THRESHOLD_DEFAULT = 0.2f
|
||||
|
||||
class AlternativesUseCase @Inject constructor(
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val searchHelperFactory: SearchV2Helper.Factory,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(manga: Manga): Flow<Manga> {
|
||||
suspend operator fun invoke(manga: Manga): Flow<Manga> = invoke(manga, MATCH_THRESHOLD_DEFAULT)
|
||||
|
||||
suspend operator fun invoke(manga: Manga, matchThreshold: Float): Flow<Manga> {
|
||||
val sources = getSources(manga.source)
|
||||
if (sources.isEmpty()) {
|
||||
return emptyFlow()
|
||||
@@ -32,23 +36,27 @@ class AlternativesUseCase @Inject constructor(
|
||||
val semaphore = Semaphore(MAX_PARALLELISM)
|
||||
return channelFlow {
|
||||
for (source in sources) {
|
||||
val repository = mangaRepositoryFactory.create(source)
|
||||
if (!repository.filterCapabilities.isSearchSupported) {
|
||||
continue
|
||||
}
|
||||
launch {
|
||||
val searchHelper = searchHelperFactory.create(source)
|
||||
val list = runCatchingCancellable {
|
||||
semaphore.withPermit {
|
||||
searchHelper(manga.title, SearchKind.TITLE)?.manga
|
||||
repository.getList(offset = 0, SortOrder.RELEVANCE, MangaListFilter(query = manga.title))
|
||||
}
|
||||
}.getOrNull()
|
||||
list?.forEach {
|
||||
launch {
|
||||
val details = runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(it.source).getDetails(it)
|
||||
}.getOrDefault(it)
|
||||
send(details)
|
||||
}.getOrDefault(emptyList())
|
||||
for (item in list) {
|
||||
if (item.matches(manga, matchThreshold)) {
|
||||
send(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.map {
|
||||
runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(it.source).getDetails(it)
|
||||
}.getOrDefault(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +68,18 @@ class AlternativesUseCase @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun Manga.matches(ref: Manga, threshold: Float): Boolean {
|
||||
return matchesTitles(title, ref.title, threshold) ||
|
||||
matchesTitles(title, ref.altTitle, threshold) ||
|
||||
matchesTitles(altTitle, ref.title, threshold) ||
|
||||
matchesTitles(altTitle, ref.altTitle, threshold)
|
||||
|
||||
}
|
||||
|
||||
private fun matchesTitles(a: String?, b: String?, threshold: Float): Boolean {
|
||||
return !a.isNullOrEmpty() && !b.isNullOrEmpty() && a.almostEquals(b, threshold)
|
||||
}
|
||||
|
||||
private fun MangaSource.priority(ref: MangaSource): Int {
|
||||
var res = 0
|
||||
if (this is MangaParserSource && ref is MangaParserSource) {
|
||||
|
||||
@@ -29,13 +29,12 @@ class AutoFixUseCase @Inject constructor(
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(mangaId: Long): Pair<Manga, Manga?> {
|
||||
val seed = checkNotNull(
|
||||
mangaDataRepository.findMangaById(mangaId, withChapters = true),
|
||||
) { "Manga $mangaId not found" }.getDetailsSafe()
|
||||
val seed = checkNotNull(mangaDataRepository.findMangaById(mangaId)) { "Manga $mangaId not found" }
|
||||
.getDetailsSafe()
|
||||
if (seed.isHealthy()) {
|
||||
return seed to null // no fix required
|
||||
}
|
||||
val replacement = alternativesUseCase(seed)
|
||||
val replacement = alternativesUseCase(seed, matchThreshold = 0.02f)
|
||||
.filter { it.isHealthy() }
|
||||
.runningFold<Manga, Manga?>(null) { best, candidate ->
|
||||
if (best == null || best < candidate) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.text.style.ForegroundColorSpan
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
@@ -27,7 +26,6 @@ import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
@@ -53,22 +51,10 @@ fun alternativeAD(
|
||||
binding.chipSource.setOnClickListener(clickListener)
|
||||
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.text = item.mangaModel.title
|
||||
with(binding.iconsView) {
|
||||
clearIcons()
|
||||
if (item.mangaModel.isSaved) addIcon(R.drawable.ic_storage)
|
||||
if (item.mangaModel.isFavorite) addIcon(R.drawable.ic_heart_outline)
|
||||
isVisible = iconsCount > 0
|
||||
}
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
binding.textViewSubtitle.text = buildSpannedString {
|
||||
if (item.chaptersCount > 0) {
|
||||
append(
|
||||
context.resources.getQuantityStringSafe(
|
||||
R.plurals.chapters,
|
||||
item.chaptersCount,
|
||||
item.chaptersCount,
|
||||
),
|
||||
)
|
||||
append(context.resources.getQuantityString(R.plurals.chapters, item.chaptersCount, item.chaptersCount))
|
||||
} else {
|
||||
append(context.getString(R.string.no_chapters))
|
||||
}
|
||||
@@ -84,10 +70,7 @@ fun alternativeAD(
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.progressView.setProgress(
|
||||
item.mangaModel.progress,
|
||||
ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,
|
||||
)
|
||||
binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
|
||||
binding.chipSource.also { chip ->
|
||||
chip.text = item.manga.source.getTitle(chip.context)
|
||||
ImageRequest.Builder(context)
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
package org.koitharu.kotatsu.alternatives.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil3.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityAlternativesBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
@@ -26,6 +30,8 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -50,7 +56,6 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
.addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||
.addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
with(viewBinding.recyclerView) {
|
||||
consumeInsetsAsPadding(Gravity.START or Gravity.END or Gravity.BOTTOM)
|
||||
setHasFixedSize(true)
|
||||
addItemDecoration(TypedListSpacingDecoration(context, addHorizontalPadding = false))
|
||||
adapter = listAdapter
|
||||
@@ -60,16 +65,33 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
viewModel.content.observe(this, listAdapter)
|
||||
viewModel.onMigrated.observeEvent(this) {
|
||||
Toast.makeText(this, R.string.migration_completed, Toast.LENGTH_SHORT).show()
|
||||
router.openDetails(it)
|
||||
startActivity(DetailsActivity.newIntent(this, it))
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaAlternativeModel, view: View) {
|
||||
when (view.id) {
|
||||
R.id.chip_source -> router.openSearch(item.manga.source, viewModel.manga.title)
|
||||
R.id.chip_source -> startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
this,
|
||||
item.manga.source,
|
||||
MangaListFilter(query = viewModel.manga.title),
|
||||
),
|
||||
)
|
||||
|
||||
R.id.button_migrate -> confirmMigration(item.manga)
|
||||
else -> router.openDetails(item.manga)
|
||||
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +114,10 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, manga: Manga) = Intent(context, AlternativesActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ import org.koitharu.kotatsu.alternatives.domain.AlternativesUseCase
|
||||
import org.koitharu.kotatsu.alternatives.domain.MigrateUseCase
|
||||
import org.koitharu.kotatsu.core.model.chaptersCount
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
@@ -36,10 +36,11 @@ class AlternativesViewModel @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val alternativesUseCase: AlternativesUseCase,
|
||||
private val migrateUseCase: MigrateUseCase,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
|
||||
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
|
||||
|
||||
val onMigrated = MutableEventFlow<Manga>()
|
||||
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
|
||||
@@ -54,7 +55,8 @@ class AlternativesViewModel @Inject constructor(
|
||||
alternativesUseCase(ref)
|
||||
.map {
|
||||
MangaAlternativeModel(
|
||||
mangaModel = mangaListMapper.toListModel(it, ListMode.GRID) as MangaGridModel,
|
||||
manga = it,
|
||||
progress = getProgress(it.id),
|
||||
referenceChapters = refCount,
|
||||
)
|
||||
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
|
||||
@@ -86,4 +88,8 @@ class AlternativesViewModel @Inject constructor(
|
||||
onMigrated.call(target)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
|
||||
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
@@ -19,15 +20,13 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
|
||||
import org.koitharu.kotatsu.core.ErrorReporterReceiver
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.powerManager
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
@@ -53,14 +52,12 @@ class AutoFixService : CoroutineIntentService() {
|
||||
val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS))
|
||||
startForeground(this)
|
||||
for (mangaId in ids) {
|
||||
powerManager.withPartialWakeLock(TAG) {
|
||||
val result = runCatchingCancellable {
|
||||
autoFixUseCase.invoke(mangaId)
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
val result = runCatchingCancellable {
|
||||
autoFixUseCase.invoke(mangaId)
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +122,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
).toBitmapOrNull(),
|
||||
)
|
||||
notification.setSubText(replacement.title)
|
||||
val intent = AppRouter.detailsIntent(applicationContext, replacement)
|
||||
val intent = DetailsActivity.newIntent(applicationContext, replacement)
|
||||
notification.setContentIntent(
|
||||
PendingIntentCompat.getActivity(
|
||||
applicationContext,
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
package org.koitharu.kotatsu.alternatives.ui
|
||||
|
||||
import org.koitharu.kotatsu.core.model.chaptersCount
|
||||
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
data class MangaAlternativeModel(
|
||||
val mangaModel: MangaGridModel,
|
||||
val manga: Manga,
|
||||
val progress: ReadingProgress?,
|
||||
private val referenceChapters: Int,
|
||||
) : ListModel {
|
||||
|
||||
val manga: Manga
|
||||
get() = mangaModel.manga
|
||||
|
||||
val chaptersCount = manga.chaptersCount()
|
||||
|
||||
val chaptersDiff: Int
|
||||
@@ -21,10 +19,4 @@ data class MangaAlternativeModel(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is MangaAlternativeModel && other.manga.id == manga.id
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? = if (previousState is MangaAlternativeModel) {
|
||||
mangaModel.getChangePayload(previousState.mangaModel)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||
|
||||
class AllBookmarksActivity : FragmentContainerActivity(AllBookmarksFragment::class.java)
|
||||
@AndroidEntryPoint
|
||||
class AllBookmarksActivity :
|
||||
BaseActivity<ActivityContainerBinding>(),
|
||||
AppBarOwner,
|
||||
SnackbarOwner {
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = viewBinding.appbar
|
||||
|
||||
override val snackbarHost: CoordinatorLayout
|
||||
get() = viewBinding.root
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityContainerBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val fm = supportFragmentManager
|
||||
if (fm.findFragmentById(R.id.container) == null) {
|
||||
fm.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container, AllBookmarksFragment::class.java, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -10,6 +9,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import coil3.ImageLoader
|
||||
@@ -18,19 +20,17 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
|
||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.GridSpanResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
@@ -39,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -85,7 +86,6 @@ class AllBookmarksFragment :
|
||||
)
|
||||
val spanSizeLookup = SpanSizeLookup()
|
||||
with(binding.recyclerView) {
|
||||
consumeInsetsAsPadding(Gravity.BOTTOM or Gravity.START or Gravity.END)
|
||||
setHasFixedSize(true)
|
||||
val spanResolver = GridSpanResolver(resources)
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
@@ -115,26 +115,26 @@ class AllBookmarksFragment :
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
if (selectionController?.onItemClick(item.pageId) != true) {
|
||||
val intent = ReaderIntent.Builder(view.context)
|
||||
val intent = ReaderActivity.IntentBuilder(view.context)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
router.openReader(intent)
|
||||
startActivity(intent)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onListHeaderClick(item: ListHeader, view: View) {
|
||||
val manga = item.payload as? Manga ?: return
|
||||
router.openDetails(manga)
|
||||
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(view, item.pageId) == true
|
||||
return selectionController?.onItemLongClick(view, item.pageId) ?: false
|
||||
}
|
||||
|
||||
override fun onItemContextClick(item: Bookmark, view: View): Boolean {
|
||||
return selectionController?.onItemContextClick(view, item.pageId) == true
|
||||
return selectionController?.onItemContextClick(view, item.pageId) ?: false
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
@@ -177,6 +177,16 @@ class AllBookmarksFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val rv = requireViewBinding().recyclerView
|
||||
rv.updatePadding(
|
||||
bottom = insets.bottom + rv.paddingTop,
|
||||
)
|
||||
rv.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable {
|
||||
|
||||
init {
|
||||
@@ -198,4 +208,16 @@ class AllBookmarksFragment :
|
||||
invalidateSpanIndexCache()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Deprecated(
|
||||
"",
|
||||
ReplaceWith(
|
||||
"BookmarksFragment()",
|
||||
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
|
||||
),
|
||||
)
|
||||
fun newInstance() = AllBookmarksFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.allowRgb565
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
|
||||
|
||||
// TODO check usages
|
||||
fun bookmarkListAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Bookmark>,
|
||||
) = adapterDelegateViewBinding<Bookmark, Bookmark, ItemBookmarkBinding>(
|
||||
{ inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
|
||||
|
||||
bind {
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||
size(CoverSizeResolver(binding.imageViewThumb))
|
||||
defaultPlaceholders(context)
|
||||
allowRgb565(true)
|
||||
bookmarkExtra(item)
|
||||
decodeRegion(item.scroll)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
@@ -26,7 +25,6 @@ class BookmarksAdapter(
|
||||
init {
|
||||
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
||||
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
||||
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
|
||||
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.core.view.updatePadding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -38,11 +42,11 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(AppRouter.KEY_SOURCE))
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
||||
val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
viewBinding.webView.configureForParser(userAgent)
|
||||
viewBinding.webView.consumeInsetsAsPadding(Gravity.START or Gravity.END or Gravity.BOTTOM)
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||
@@ -55,7 +59,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
onTitleChanged(
|
||||
intent?.getStringExtra(AppRouter.KEY_TITLE) ?: getString(R.string.loading_),
|
||||
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
||||
url,
|
||||
)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
@@ -76,8 +80,14 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
}
|
||||
|
||||
R.id.action_browser -> {
|
||||
if (!router.openExternalBrowser(viewBinding.webView.url.orEmpty(), item.title)) {
|
||||
Snackbar.make(viewBinding.webView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
val url = viewBinding.webView.url?.toUriOrNull()
|
||||
if (url != null) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, item.title))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -115,4 +125,28 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
override fun onHistoryChanged() {
|
||||
onBackPressedCallback.onHistoryChanged()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.appbar.updatePadding(
|
||||
top = insets.top,
|
||||
)
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_TITLE = "title"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent {
|
||||
return Intent(context, BrowserActivity::class.java)
|
||||
.setData(Uri.parse(url))
|
||||
.putExtra(EXTRA_TITLE, title)
|
||||
.putExtra(EXTRA_SOURCE, source?.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@@ -30,9 +28,6 @@ class CaptchaNotifier(
|
||||
if (!context.checkNotificationPermission(CHANNEL_ID)) {
|
||||
return
|
||||
}
|
||||
if (exception.source != null && SourceSettings(context, exception.source).isCaptchaNotificationsDisabled) {
|
||||
return
|
||||
}
|
||||
val manager = NotificationManagerCompat.from(context)
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(context.getString(R.string.captcha_required))
|
||||
@@ -43,7 +38,7 @@ class CaptchaNotifier(
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
|
||||
val intent = AppRouter.cloudFlareResolveIntent(context, exception)
|
||||
val intent = CloudFlareActivity.newIntent(context, exception)
|
||||
.setData(exception.url.toUri())
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(channel.name)
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.yield
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -57,12 +62,12 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
return
|
||||
}
|
||||
cfClient = CloudFlareClient(cookieJar, this, url)
|
||||
viewBinding.webView.configureForParser(intent?.getStringExtra(AppRouter.KEY_USER_AGENT))
|
||||
viewBinding.webView.consumeInsetsAsPadding(Gravity.START or Gravity.END or Gravity.BOTTOM)
|
||||
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
|
||||
viewBinding.webView.webViewClient = cfClient
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
||||
onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
if (savedInstanceState == null) {
|
||||
onTitleChanged(getString(R.string.loading_), url)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
@@ -84,6 +89,17 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.appbar.updatePadding(
|
||||
top = insets.top,
|
||||
)
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
viewBinding.webView.stopLoading()
|
||||
@@ -124,7 +140,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult = RESULT_OK
|
||||
val source = intent?.getStringExtra(AppRouter.KEY_SOURCE)
|
||||
val source = intent?.getStringExtra(ARG_SOURCE)
|
||||
if (source != null) {
|
||||
CaptchaNotifier(this).dismiss(MangaSource(source))
|
||||
}
|
||||
@@ -166,16 +182,38 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
class Contract : ActivityResultContract<CloudFlareProtectedException, Boolean>() {
|
||||
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
|
||||
return AppRouter.cloudFlareResolveIntent(context, input)
|
||||
return newIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
|
||||
return resultCode == RESULT_OK
|
||||
return resultCode == Activity.RESULT_OK
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareActivity"
|
||||
private const val ARG_UA = "ua"
|
||||
private const val ARG_SOURCE = "_source"
|
||||
|
||||
fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
|
||||
context = context,
|
||||
url = exception.url,
|
||||
source = exception.source,
|
||||
headers = exception.headers,
|
||||
)
|
||||
|
||||
private fun newIntent(
|
||||
context: Context,
|
||||
url: String,
|
||||
source: MangaSource?,
|
||||
headers: Headers?,
|
||||
) = Intent(context, CloudFlareActivity::class.java).apply {
|
||||
data = url.toUri()
|
||||
putExtra(ARG_SOURCE, source?.name)
|
||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||
putExtra(ARG_UA, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.os.Build
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.text.Html
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.room.InvalidationTracker
|
||||
import androidx.work.WorkManager
|
||||
import coil3.ImageLoader
|
||||
@@ -77,12 +76,6 @@ interface AppModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@LocalizedAppContext
|
||||
fun provideLocalizedContext(
|
||||
@ApplicationContext context: Context,
|
||||
): Context = ContextCompat.getContextForLanguage(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkState(
|
||||
|
||||
@@ -13,6 +13,7 @@ import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
@@ -25,14 +26,12 @@ import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.os.AppValidator
|
||||
import org.koitharu.kotatsu.core.os.RomCompat
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
|
||||
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
|
||||
import java.security.Security
|
||||
import javax.inject.Inject
|
||||
@@ -83,14 +82,17 @@ open class BaseApp : Application(), Configuration.Provider {
|
||||
return
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||
// TLS 1.3 support for Android < 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
setupActivityLifecycleCallbacks()
|
||||
processLifecycleScope.launch {
|
||||
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.getOrNull().toString())
|
||||
ACRA.errorReporter.putCustomData("isMiui", RomCompat.isMiui.getOrNull().toString())
|
||||
val isOriginalApp = withContext(Dispatchers.Default) {
|
||||
appValidator.isOriginalApp
|
||||
}
|
||||
ACRA.errorReporter.putCustomData("isOriginalApp", isOriginalApp.toString())
|
||||
}
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
setupDatabaseObservers()
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.net.Uri
|
||||
import android.os.BadParcelableException
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
@@ -16,19 +15,20 @@ import org.koitharu.kotatsu.core.util.ext.report
|
||||
class ErrorReporterReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return
|
||||
val e = intent?.getSerializableExtraCompat<Throwable>(EXTRA_ERROR) ?: return
|
||||
e.report()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_ERROR = "err"
|
||||
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
|
||||
|
||||
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try {
|
||||
val intent = Intent(context, ErrorReporterReceiver::class.java)
|
||||
intent.setAction(ACTION_REPORT)
|
||||
intent.setData(Uri.parse("err://${e.hashCode()}"))
|
||||
intent.putExtra(AppRouter.KEY_ERROR, e)
|
||||
intent.putExtra(EXTRA_ERROR, e)
|
||||
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
|
||||
} catch (e: BadParcelableException) {
|
||||
e.printStackTraceDebug()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.core
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Target(
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
AnnotationTarget.FIELD,
|
||||
)
|
||||
annotation class LocalizedAppContext
|
||||
@@ -16,7 +16,6 @@ class BackupEntry(
|
||||
CATEGORIES("categories"),
|
||||
FAVOURITES("favourites"),
|
||||
SETTINGS("settings"),
|
||||
SETTINGS_READER_GRID("reader_grid"),
|
||||
BOOKMARKS("bookmarks"),
|
||||
SOURCES("sources"),
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.progress.Progress
|
||||
import org.koitharu.kotatsu.parsers.util.json.asTypedList
|
||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.data.TapGridSettings
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -21,7 +18,6 @@ private const val PAGE_SIZE = 10
|
||||
class BackupRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val settings: AppSettings,
|
||||
private val tapGridSettings: TapGridSettings,
|
||||
) {
|
||||
|
||||
suspend fun dumpHistory(): BackupEntry {
|
||||
@@ -107,14 +103,6 @@ class BackupRepository @Inject constructor(
|
||||
return entry
|
||||
}
|
||||
|
||||
fun dumpReaderGridSettings(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.Name.SETTINGS_READER_GRID, JSONArray())
|
||||
val settingsDump = tapGridSettings.getAllValues()
|
||||
val json = JsonSerializer(settingsDump).toJson()
|
||||
entry.data.put(json)
|
||||
return entry
|
||||
}
|
||||
|
||||
suspend fun dumpSources(): BackupEntry {
|
||||
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
|
||||
val all = db.getSourcesDao().findAll()
|
||||
@@ -140,11 +128,9 @@ class BackupRepository @Inject constructor(
|
||||
return if (timestamp == 0L) null else Date(timestamp)
|
||||
}
|
||||
|
||||
suspend fun restoreHistory(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
|
||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
val list = entry.data.asTypedList<JSONObject>()
|
||||
outProgress?.emit(Progress(progress = 0, total = list.size))
|
||||
for ((index, item) in list.withIndex()) {
|
||||
for (item in entry.data.asTypedList<JSONObject>()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
@@ -158,7 +144,6 @@ class BackupRepository @Inject constructor(
|
||||
db.getHistoryDao().upsert(history)
|
||||
}
|
||||
}
|
||||
outProgress?.emit(Progress(progress = index, total = list.size))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -174,11 +159,9 @@ class BackupRepository @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun restoreFavourites(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
|
||||
suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
val list = entry.data.asTypedList<JSONObject>()
|
||||
outProgress?.emit(Progress(progress = 0, total = list.size))
|
||||
for ((index, item) in list.withIndex()) {
|
||||
for (item in entry.data.asTypedList<JSONObject>()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
@@ -192,7 +175,6 @@ class BackupRepository @Inject constructor(
|
||||
db.getFavouritesDao().upsert(favourite)
|
||||
}
|
||||
}
|
||||
outProgress?.emit(Progress(progress = index, total = list.size))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -239,14 +221,4 @@ class BackupRepository @Inject constructor(
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun restoreReaderGridSettings(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.asTypedList<JSONObject>()) {
|
||||
result += runCatchingCancellable {
|
||||
tapGridSettings.upsertAll(JsonDeserializer(item).toMap())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@ class CompositeResult {
|
||||
}
|
||||
}
|
||||
|
||||
operator fun plusAssign(error: Throwable) {
|
||||
errors.add(error)
|
||||
}
|
||||
|
||||
operator fun plusAssign(other: CompositeResult) {
|
||||
this.successCount += other.successCount
|
||||
this.errors += other.errors
|
||||
|
||||
@@ -28,16 +28,15 @@ class JsonDeserializer(private val json: JSONObject) {
|
||||
fun toMangaEntity() = MangaEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
altTitles = json.getStringOrNull("alt_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),
|
||||
contentRating = json.getStringOrNull("content_rating"),
|
||||
coverUrl = json.getString("cover_url"),
|
||||
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
||||
state = json.getStringOrNull("state"),
|
||||
authors = json.getStringOrNull("author"),
|
||||
author = json.getStringOrNull("author"),
|
||||
source = json.getString("source"),
|
||||
)
|
||||
|
||||
|
||||
@@ -58,16 +58,15 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
JSONObject().apply {
|
||||
put("id", e.id)
|
||||
put("title", e.title)
|
||||
put("alt_title", e.altTitles)
|
||||
put("alt_title", e.altTitle)
|
||||
put("url", e.url)
|
||||
put("public_url", e.publicUrl)
|
||||
put("rating", e.rating)
|
||||
put("nsfw", e.isNsfw)
|
||||
put("content_rating", e.contentRating)
|
||||
put("cover_url", e.coverUrl)
|
||||
put("large_cover_url", e.largeCoverUrl)
|
||||
put("state", e.state)
|
||||
put("author", e.authors)
|
||||
put("author", e.author)
|
||||
put("source", e.source)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.CheckResult
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TelegramBackupUploader @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
@BaseHttpClient private val client: OkHttpClient,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
private val botToken = context.getString(R.string.tg_backup_bot_token)
|
||||
|
||||
suspend fun uploadBackup(file: File) {
|
||||
val requestBody = file.asRequestBody("application/zip".toMediaTypeOrNull())
|
||||
val multipartBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("chat_id", requireChatId())
|
||||
.addFormDataPart("document", file.name, requestBody)
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(urlOf("sendDocument").build())
|
||||
.post(multipartBody)
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
suspend fun sendTestMessage() {
|
||||
val request = Request.Builder()
|
||||
.url(urlOf("getMe").build())
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
sendMessage(context.getString(R.string.backup_tg_echo))
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun openBotInApp(router: AppRouter): Boolean {
|
||||
val botUsername = context.getString(R.string.tg_backup_bot_name)
|
||||
return router.openExternalBrowser("tg://resolve?domain=$botUsername") ||
|
||||
router.openExternalBrowser("https://t.me/$botUsername")
|
||||
}
|
||||
|
||||
private suspend fun sendMessage(message: String) {
|
||||
val url = urlOf("sendMessage")
|
||||
.addQueryParameter("chat_id", requireChatId())
|
||||
.addQueryParameter("text", message)
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
client.newCall(request).await().consume()
|
||||
}
|
||||
|
||||
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
|
||||
"Telegram chat ID not set in settings"
|
||||
}
|
||||
|
||||
private fun Response.consume() {
|
||||
if (isSuccessful) {
|
||||
closeQuietly()
|
||||
return
|
||||
}
|
||||
val jo = parseJson()
|
||||
if (!jo.getBooleanOrDefault("ok", true)) {
|
||||
throw RuntimeException(jo.getStringOrNull("description"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun urlOf(method: String) = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("api.telegram.org")
|
||||
.addPathSegment("bot$botToken")
|
||||
.addPathSegment(method)
|
||||
}
|
||||
@@ -12,13 +12,11 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
|
||||
import org.koitharu.kotatsu.core.db.dao.ChaptersDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaDao
|
||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TagsDao
|
||||
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
|
||||
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
@@ -38,9 +36,6 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration22To23
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration23To24
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration24To23
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration24To25
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
|
||||
@@ -68,14 +63,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
const val DATABASE_VERSION = 25
|
||||
const val DATABASE_VERSION = 23
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, ChapterEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class,
|
||||
TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ScrobblingEntity::class,
|
||||
MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
|
||||
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
|
||||
],
|
||||
version = DATABASE_VERSION,
|
||||
)
|
||||
@@ -108,8 +103,6 @@ abstract class MangaDatabase : RoomDatabase() {
|
||||
abstract fun getStatsDao(): StatsDao
|
||||
|
||||
abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
|
||||
|
||||
abstract fun getChaptersDao(): ChaptersDao
|
||||
}
|
||||
|
||||
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
@@ -135,9 +128,6 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||
Migration20To21(),
|
||||
Migration21To22(),
|
||||
Migration22To23(),
|
||||
Migration23To24(),
|
||||
Migration24To23(),
|
||||
Migration24To25(),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
|
||||
@@ -7,4 +7,3 @@ const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||
const val TABLE_HISTORY = "history"
|
||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||
const val TABLE_SOURCES = "sources"
|
||||
const val TABLE_CHAPTERS = "chapters"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
|
||||
|
||||
@Dao
|
||||
abstract class ChaptersDao {
|
||||
|
||||
@Query("SELECT * FROM chapters WHERE manga_id = :mangaId ORDER BY `index` ASC")
|
||||
abstract suspend fun findAll(mangaId: Long): List<ChapterEntity>
|
||||
|
||||
@Query("DELETE FROM chapters WHERE manga_id = :mangaId")
|
||||
abstract suspend fun deleteAll(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM chapters WHERE manga_id NOT IN (SELECT manga_id FROM history WHERE deleted_at = 0) AND manga_id NOT IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
|
||||
abstract suspend fun gc()
|
||||
|
||||
@Transaction
|
||||
open suspend fun replaceAll(mangaId: Long, entities: Collection<ChapterEntity>) {
|
||||
deleteAll(mangaId)
|
||||
insert(entities)
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(entities: Collection<ChapterEntity>)
|
||||
}
|
||||
@@ -20,9 +20,6 @@ abstract class MangaDao {
|
||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||
abstract suspend fun find(id: Long): MangaWithTags?
|
||||
|
||||
@Query("SELECT EXISTS(SELECT * FROM manga WHERE manga_id = :id)")
|
||||
abstract suspend operator fun contains(id: Long): Boolean
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
|
||||
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
|
||||
@@ -58,19 +55,6 @@ abstract class MangaDao {
|
||||
@Delete
|
||||
abstract suspend fun delete(subjects: Collection<MangaEntity>)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM manga WHERE NOT EXISTS(SELECT * FROM history WHERE history.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM favourites WHERE favourites.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM bookmarks WHERE bookmarks.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM suggestions WHERE suggestions.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM scrobblings WHERE scrobblings.manga_id == manga.manga_id)
|
||||
AND NOT EXISTS(SELECT * FROM local_index WHERE local_index.manga_id == manga.manga_id)
|
||||
AND manga.manga_id NOT IN (:idsToKeep)
|
||||
""",
|
||||
)
|
||||
abstract suspend fun cleanup(idsToKeep: Set<Long>)
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
|
||||
upsert(manga)
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
@@ -60,11 +61,21 @@ abstract class MangaSourcesDao {
|
||||
@Query("SELECT * FROM sources WHERE pinned = 1")
|
||||
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
|
||||
|
||||
fun observeAll(enabledOnly: Boolean, order: SourcesSortOrder): Flow<List<MangaSourceEntity>> =
|
||||
observeImpl(getQuery(enabledOnly, order))
|
||||
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
suspend fun findAll(enabledOnly: Boolean, order: SourcesSortOrder): List<MangaSourceEntity> =
|
||||
findAllImpl(getQuery(enabledOnly, order))
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
||||
return observeImpl(query)
|
||||
}
|
||||
|
||||
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
|
||||
return findAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
|
||||
@@ -90,17 +101,6 @@ abstract class MangaSourcesDao {
|
||||
@RawQuery
|
||||
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
|
||||
|
||||
private fun getQuery(enabledOnly: Boolean, order: SourcesSortOrder) = SimpleSQLiteQuery(
|
||||
buildString {
|
||||
append("SELECT * FROM sources ")
|
||||
if (enabledOnly) {
|
||||
append("WHERE enabled = 1 ")
|
||||
}
|
||||
append("ORDER BY pinned DESC, ")
|
||||
append(getOrderBy(order))
|
||||
},
|
||||
)
|
||||
|
||||
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
|
||||
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
||||
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_CHAPTERS
|
||||
|
||||
@Entity(
|
||||
tableName = TABLE_CHAPTERS,
|
||||
primaryKeys = ["manga_id", "chapter_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class ChapterEntity(
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "name") val title: String,
|
||||
@ColumnInfo(name = "number") val number: Float,
|
||||
@ColumnInfo(name = "volume") val volume: Int,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "scanlator") val scanlator: String?,
|
||||
@ColumnInfo(name = "upload_date") val uploadDate: Long,
|
||||
@ColumnInfo(name = "branch") val branch: String?,
|
||||
@ColumnInfo(name = "source") val source: String,
|
||||
@ColumnInfo(name = "index") val index: Int,
|
||||
)
|
||||
@@ -1,20 +1,14 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.longHashCode
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.toArraySet
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
|
||||
private const val VALUES_DIVIDER = '\n'
|
||||
|
||||
// Entity to model
|
||||
|
||||
fun TagEntity.toMangaTag() = MangaTag(
|
||||
@@ -27,42 +21,26 @@ fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
||||
|
||||
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
|
||||
|
||||
fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga(
|
||||
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||
id = this.id,
|
||||
title = this.title,
|
||||
altTitles = this.altTitles?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
|
||||
altTitle = this.altTitle,
|
||||
state = this.state?.let { MangaState(it) },
|
||||
rating = this.rating,
|
||||
contentRating = ContentRating(this.contentRating)
|
||||
?: if (isNsfw) ContentRating.ADULT else null,
|
||||
isNsfw = this.isNsfw,
|
||||
url = this.url,
|
||||
publicUrl = this.publicUrl,
|
||||
coverUrl = this.coverUrl,
|
||||
largeCoverUrl = this.largeCoverUrl,
|
||||
authors = this.authors?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
|
||||
author = this.author,
|
||||
source = MangaSource(this.source),
|
||||
tags = tags,
|
||||
chapters = chapters?.toMangaChapters(),
|
||||
)
|
||||
|
||||
fun MangaWithTags.toManga(chapters: List<ChapterEntity>? = null) = manga.toManga(tags.toMangaTags(), chapters)
|
||||
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
|
||||
|
||||
fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() }
|
||||
|
||||
fun ChapterEntity.toMangaChapter() = MangaChapter(
|
||||
id = chapterId,
|
||||
title = title.nullIfEmpty(),
|
||||
number = number,
|
||||
volume = volume,
|
||||
url = url,
|
||||
scanlator = scanlator,
|
||||
uploadDate = uploadDate,
|
||||
branch = branch,
|
||||
source = MangaSource(source),
|
||||
)
|
||||
|
||||
fun Collection<ChapterEntity>.toMangaChapters() = map { it.toMangaChapter() }
|
||||
|
||||
// Model to entity
|
||||
|
||||
fun Manga.toEntity() = MangaEntity(
|
||||
@@ -72,13 +50,12 @@ fun Manga.toEntity() = MangaEntity(
|
||||
source = source.name,
|
||||
largeCoverUrl = largeCoverUrl,
|
||||
coverUrl = coverUrl.orEmpty(),
|
||||
altTitles = altTitles.joinToString(VALUES_DIVIDER.toString()),
|
||||
altTitle = altTitle,
|
||||
rating = rating,
|
||||
isNsfw = isNsfw,
|
||||
contentRating = contentRating?.name,
|
||||
state = state?.name,
|
||||
title = title,
|
||||
authors = authors.joinToString(VALUES_DIVIDER.toString()),
|
||||
author = author,
|
||||
)
|
||||
|
||||
fun MangaTag.toEntity() = TagEntity(
|
||||
@@ -90,22 +67,6 @@ fun MangaTag.toEntity() = TagEntity(
|
||||
|
||||
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
|
||||
|
||||
fun Iterable<IndexedValue<MangaChapter>>.toEntities(mangaId: Long) = map { (index, chapter) ->
|
||||
ChapterEntity(
|
||||
chapterId = chapter.id,
|
||||
mangaId = mangaId,
|
||||
title = chapter.title.orEmpty(),
|
||||
number = chapter.number,
|
||||
volume = chapter.volume,
|
||||
url = chapter.url,
|
||||
scanlator = chapter.scanlator,
|
||||
uploadDate = chapter.uploadDate,
|
||||
branch = chapter.branch,
|
||||
source = chapter.source.name,
|
||||
index = index,
|
||||
)
|
||||
}
|
||||
|
||||
// Other
|
||||
|
||||
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
|
||||
@@ -115,7 +76,3 @@ fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
|
||||
fun MangaState(name: String): MangaState? = runCatching {
|
||||
MangaState.valueOf(name)
|
||||
}.getOrNull()
|
||||
|
||||
fun ContentRating(name: String?): ContentRating? = runCatching {
|
||||
ContentRating.valueOf(name ?: return@runCatching null)
|
||||
}.getOrNull()
|
||||
|
||||
@@ -10,15 +10,14 @@ data class MangaEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "alt_title") val altTitles: String?,
|
||||
@ColumnInfo(name = "alt_title") val altTitle: String?,
|
||||
@ColumnInfo(name = "url") val url: String,
|
||||
@ColumnInfo(name = "public_url") val publicUrl: String,
|
||||
@ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1
|
||||
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
|
||||
@ColumnInfo(name = "content_rating") val contentRating: String?,
|
||||
@ColumnInfo(name = "nsfw") val isNsfw: Boolean, // TODO change to contentRating
|
||||
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
|
||||
@ColumnInfo(name = "state") val state: String?,
|
||||
@ColumnInfo(name = "author") val authors: String?,
|
||||
@ColumnInfo(name = "author") val author: String?,
|
||||
@ColumnInfo(name = "source") val source: String,
|
||||
)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration23To24 : Migration(23, 24) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `chapters` (`chapter_id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` REAL NOT NULL, `volume` INTEGER NOT NULL, `url` TEXT NOT NULL, `scanlator` TEXT, `upload_date` INTEGER NOT NULL, `branch` TEXT, `source` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `chapter_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration24To23 : Migration(24, 23) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS `chapters`")
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration24To25 : Migration(24, 25) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE manga ADD COLUMN content_rating TEXT DEFAULT NULL")
|
||||
db.execSQL("UPDATE manga SET content_rating = 'ADULT' WHERE nsfw = 1")
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||
@@ -31,10 +32,10 @@ class DialogErrorObserver(
|
||||
if (canResolve(value)) {
|
||||
dialogBuilder.setPositiveButton(ExceptionResolver.getResolveStringId(value), listener)
|
||||
} else if (value is ParseException) {
|
||||
val router = router()
|
||||
if (router != null && value.isSerializable()) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && value.isSerializable()) {
|
||||
dialogBuilder.setPositiveButton(R.string.details) { _, _ ->
|
||||
router.showErrorDialog(value)
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -12,7 +11,6 @@ import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
|
||||
@@ -35,8 +33,6 @@ abstract class ErrorObserver(
|
||||
return resolver != null && ExceptionResolver.canResolve(error)
|
||||
}
|
||||
|
||||
protected fun router() = fragment?.router ?: (activity as? FragmentActivity)?.router
|
||||
|
||||
private fun isAlive(): Boolean {
|
||||
return when {
|
||||
fragment != null -> fragment.view != null
|
||||
@@ -48,7 +44,7 @@ abstract class ErrorObserver(
|
||||
protected fun resolve(error: Throwable) {
|
||||
if (isAlive()) {
|
||||
lifecycleScope.launch {
|
||||
val isResolved = resolver?.resolve(error) == true
|
||||
val isResolved = resolver?.resolve(error) ?: false
|
||||
if (isActive) {
|
||||
onResolved?.accept(isResolved)
|
||||
}
|
||||
|
||||
@@ -5,20 +5,19 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.collection.MutableScatterMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.restartApplication
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
@@ -27,6 +26,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import javax.inject.Provider
|
||||
@@ -49,8 +49,8 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
handleActivityResult(CloudFlareActivity.TAG, it)
|
||||
}
|
||||
|
||||
fun showErrorDetails(e: Throwable, url: String? = null) {
|
||||
host.router()?.showErrorDialog(e, url)
|
||||
fun showDetails(e: Throwable, url: String?) {
|
||||
ErrorDetailsDialog.show(host.getChildFragmentManager(), e, url)
|
||||
}
|
||||
|
||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||
@@ -63,7 +63,9 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
}
|
||||
|
||||
is ProxyConfigException -> {
|
||||
host.router()?.openProxySettings()
|
||||
host.withContext {
|
||||
startActivity(SettingsActivity.newProxySettingsIntent(this))
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -83,7 +85,9 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
true
|
||||
} else {
|
||||
host.withContext {
|
||||
authHelper.startAuth(this, e.scrobbler).onFailure(::showErrorDetails)
|
||||
authHelper.startAuth(this, e.scrobbler).onFailure {
|
||||
showDetails(it, null)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -102,12 +106,12 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
sourceAuthContract.launch(source)
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String) {
|
||||
host.router()?.openBrowser(url, null, null)
|
||||
private fun openInBrowser(url: String) = host.withContext {
|
||||
startActivity(BrowserActivity.newIntent(this, url, null, null))
|
||||
}
|
||||
|
||||
private fun openAlternatives(manga: Manga) {
|
||||
host.router()?.openAlternatives(manga)
|
||||
private fun openAlternatives(manga: Manga) = host.withContext {
|
||||
startActivity(AlternativesActivity.newIntent(this, manga))
|
||||
}
|
||||
|
||||
private fun handleActivityResult(tag: String, result: Boolean) {
|
||||
@@ -136,12 +140,6 @@ class ExceptionResolver @AssistedInject constructor(
|
||||
getContext()?.apply(block)
|
||||
}
|
||||
|
||||
private fun Host.router(): AppRouter? = when (this) {
|
||||
is FragmentActivity -> router
|
||||
is Fragment -> router
|
||||
else -> null
|
||||
}
|
||||
|
||||
interface Host : ActivityResultCaller {
|
||||
|
||||
fun getChildFragmentManager(): FragmentManager
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.isSerializable
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
@@ -32,10 +33,10 @@ class SnackbarErrorObserver(
|
||||
resolve(value)
|
||||
}
|
||||
} else if (value is ParseException) {
|
||||
val router = router()
|
||||
if (router != null && value.isSerializable()) {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && value.isSerializable()) {
|
||||
snackbar.setAction(R.string.details) {
|
||||
router.showErrorDialog(value)
|
||||
ErrorDetailsDialog.show(fm, value, value.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -86,8 +85,8 @@ class AppUpdateRepository @Inject constructor(
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
suspend fun isUpdateSupported(): Boolean {
|
||||
return BuildConfig.BUILD_TYPE != BUILD_TYPE_RELEASE || appValidator.isOriginalApp.getOrNull() == true
|
||||
fun isUpdateSupported(): Boolean {
|
||||
return BuildConfig.BUILD_TYPE != BUILD_TYPE_RELEASE || appValidator.isOriginalApp
|
||||
}
|
||||
|
||||
suspend fun getCurrentVersionChangelog(): String? {
|
||||
|
||||
@@ -4,26 +4,26 @@ import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import org.aomedia.avif.android.AvifDecoder
|
||||
import org.aomedia.avif.android.AvifDecoder.Info
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.MimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.file.Files
|
||||
|
||||
object BitmapDecoderCompat {
|
||||
|
||||
private const val FORMAT_AVIF = "avif"
|
||||
|
||||
@Blocking
|
||||
fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) {
|
||||
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
|
||||
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||
@@ -33,7 +33,7 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun decode(stream: InputStream, type: MimeType?, isMutable: Boolean = false): Bitmap {
|
||||
fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap {
|
||||
val format = type?.subtype
|
||||
if (format == FORMAT_AVIF) {
|
||||
return decodeAvif(stream.toByteBuffer())
|
||||
@@ -51,20 +51,12 @@ object BitmapDecoderCompat {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun probeMimeType(file: File): MimeType? {
|
||||
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
|
||||
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
|
||||
} else {
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private fun detectBitmapType(file: File): MimeType? = runCatchingCancellable {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
return options.outMimeType?.toMimeTypeOrNull()
|
||||
}.getOrNull()
|
||||
|
||||
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
|
||||
bitmap ?: throw ImageDecodeException(null, format)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil3.ImageLoader
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
@@ -11,7 +12,6 @@ import coil3.toAndroidUri
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Path.Companion.toPath
|
||||
import okio.openZip
|
||||
import org.koitharu.kotatsu.core.util.MimeTypes
|
||||
import org.koitharu.kotatsu.core.util.ext.isZipUri
|
||||
import coil3.Uri as CoilUri
|
||||
|
||||
@@ -23,10 +23,9 @@ class CbzFetcher(
|
||||
override suspend fun fetch() = runInterruptible {
|
||||
val filePath = uri.schemeSpecificPart.toPath()
|
||||
val entryName = requireNotNull(uri.fragment)
|
||||
val fs = options.fileSystem.openZip(filePath)
|
||||
SourceFetchResult(
|
||||
source = ImageSource(entryName.toPath(), fs, closeable = fs),
|
||||
mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
|
||||
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.annotation.DrawableRes
|
||||
@@ -169,24 +168,3 @@ private fun SpannableStringBuilder.appendTagsSummary(filter: MangaListFilter) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MangaChapter.getLocalizedTitle(resources: Resources, index: Int = -1): String {
|
||||
title?.let {
|
||||
if (it.isNotBlank()) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
val num = numberString()
|
||||
val vol = volumeString()
|
||||
return when {
|
||||
num != null && vol != null -> resources.getString(R.string.chapter_volume_number, vol, num)
|
||||
num != null -> resources.getString(R.string.chapter_number, num)
|
||||
index > 0 -> resources.getString(
|
||||
R.string.chapters_time_pattern,
|
||||
resources.getString(R.string.unnamed_chapter),
|
||||
index.toString(),
|
||||
)
|
||||
|
||||
else -> resources.getString(R.string.unnamed_chapter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,11 @@ package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.inSpans
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
@@ -105,16 +100,3 @@ fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||
) {
|
||||
append(context.getString(R.string.nsfw))
|
||||
}
|
||||
|
||||
fun SpannableStringBuilder.appendIcon(textView: TextView, @DrawableRes resId: Int): SpannableStringBuilder {
|
||||
val icon = ContextCompat.getDrawable(textView.context, resId) ?: return this
|
||||
icon.setTintList(textView.textColors)
|
||||
val size = textView.lineHeight
|
||||
icon.setBounds(0, 0, size, size)
|
||||
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ImageSpan.ALIGN_CENTER
|
||||
} else {
|
||||
ImageSpan.ALIGN_BOTTOM
|
||||
}
|
||||
return inSpans(ImageSpan(icon, alignment)) { append(' ') }
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
|
||||
fun ListFilterOption.toChipModel(isChecked: Boolean) = ChipsView.ChipModel(
|
||||
title = titleText,
|
||||
titleResId = titleResId,
|
||||
icon = iconResId,
|
||||
iconData = getIconData(),
|
||||
isChecked = isChecked,
|
||||
data = this,
|
||||
)
|
||||
@@ -17,7 +17,7 @@ data class ParcelableChapter(
|
||||
override fun create(parcel: Parcel) = ParcelableChapter(
|
||||
MangaChapter(
|
||||
id = parcel.readLong(),
|
||||
title = parcel.readString(),
|
||||
name = parcel.readString().orEmpty(),
|
||||
number = parcel.readFloat(),
|
||||
volume = parcel.readInt(),
|
||||
url = parcel.readString().orEmpty(),
|
||||
@@ -30,7 +30,7 @@ data class ParcelableChapter(
|
||||
|
||||
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
|
||||
parcel.writeLong(id)
|
||||
parcel.writeString(title)
|
||||
parcel.writeString(name)
|
||||
parcel.writeFloat(number)
|
||||
parcel.writeInt(volume)
|
||||
parcel.writeString(url)
|
||||
|
||||
@@ -7,8 +7,6 @@ import kotlinx.parcelize.Parcelize
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.readStringSet
|
||||
import org.koitharu.kotatsu.core.util.ext.writeStringSet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@Parcelize
|
||||
@@ -22,7 +20,7 @@ data class ParcelableManga(
|
||||
override fun ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) {
|
||||
parcel.writeLong(id)
|
||||
parcel.writeString(title)
|
||||
parcel.writeStringSet(altTitles)
|
||||
parcel.writeString(altTitle)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(publicUrl)
|
||||
parcel.writeFloat(rating)
|
||||
@@ -32,7 +30,7 @@ data class ParcelableManga(
|
||||
parcel.writeString(description.takeIf { withDescription })
|
||||
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
||||
parcel.writeSerializable(state)
|
||||
parcel.writeStringSet(authors)
|
||||
parcel.writeString(author)
|
||||
parcel.writeString(source.name)
|
||||
}
|
||||
|
||||
@@ -40,7 +38,7 @@ data class ParcelableManga(
|
||||
Manga(
|
||||
id = parcel.readLong(),
|
||||
title = requireNotNull(parcel.readString()),
|
||||
altTitles = parcel.readStringSet(),
|
||||
altTitle = parcel.readString(),
|
||||
url = requireNotNull(parcel.readString()),
|
||||
publicUrl = requireNotNull(parcel.readString()),
|
||||
rating = parcel.readFloat(),
|
||||
@@ -50,7 +48,7 @@ data class ParcelableManga(
|
||||
description = parcel.readString(),
|
||||
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
|
||||
state = parcel.readSerializableCompat(),
|
||||
authors = parcel.readStringSet(),
|
||||
author = parcel.readString(),
|
||||
chapters = null,
|
||||
source = MangaSource(parcel.readString()),
|
||||
),
|
||||
|
||||
@@ -30,7 +30,6 @@ object MangaListFilterParceler : Parceler<MangaListFilter> {
|
||||
parcel.writeInt(year)
|
||||
parcel.writeInt(yearFrom)
|
||||
parcel.writeInt(yearTo)
|
||||
parcel.writeString(author)
|
||||
}
|
||||
|
||||
override fun create(parcel: Parcel) = MangaListFilter(
|
||||
@@ -46,7 +45,6 @@ object MangaListFilterParceler : Parceler<MangaListFilter> {
|
||||
year = parcel.readInt(),
|
||||
yearFrom = parcel.readInt(),
|
||||
yearTo = parcel.readInt(),
|
||||
author = parcel.readString(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,731 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
|
||||
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.MangaSourceInfo
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPage
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.ui.dialog.BigButtonsAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
|
||||
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoSheet
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
|
||||
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
|
||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
|
||||
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
|
||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.isNullOrEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.mapToArray
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateActivity
|
||||
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
|
||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
|
||||
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
|
||||
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
|
||||
import org.koitharu.kotatsu.stats.ui.StatsActivity
|
||||
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
|
||||
|
||||
class AppRouter private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
private val fragment: Fragment?,
|
||||
) {
|
||||
|
||||
constructor(activity: FragmentActivity) : this(activity, null)
|
||||
|
||||
constructor(fragment: Fragment) : this(null, fragment)
|
||||
|
||||
private val settings: AppSettings by lazy {
|
||||
EntryPointAccessors.fromApplication<AppRouterEntryPoint>(checkNotNull(contextOrNull())).settings
|
||||
}
|
||||
|
||||
/** Activities **/
|
||||
|
||||
fun openList(source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?) {
|
||||
startActivity(listIntent(contextOrNull() ?: return, source, filter, sortOrder))
|
||||
}
|
||||
|
||||
fun openList(tag: MangaTag) = openList(tag.source, MangaListFilter(tags = setOf(tag)), null)
|
||||
|
||||
fun openSearch(query: String, kind: SearchKind = SearchKind.SIMPLE) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, SearchActivity::class.java)
|
||||
.putExtra(KEY_QUERY, query)
|
||||
.putExtra(KEY_KIND, kind),
|
||||
)
|
||||
}
|
||||
|
||||
fun openSearch(source: MangaSource, query: String) = openList(source, MangaListFilter(query = query), null)
|
||||
|
||||
fun openDetails(manga: Manga) {
|
||||
startActivity(detailsIntent(contextOrNull() ?: return, manga))
|
||||
}
|
||||
|
||||
fun openDetails(mangaId: Long) {
|
||||
startActivity(detailsIntent(contextOrNull() ?: return, mangaId))
|
||||
}
|
||||
|
||||
fun openDetails(link: Uri) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, DetailsActivity::class.java)
|
||||
.setData(link),
|
||||
)
|
||||
}
|
||||
|
||||
fun openReader(manga: Manga, anchor: View? = null) {
|
||||
openReader(
|
||||
ReaderIntent.Builder(contextOrNull() ?: return)
|
||||
.manga(manga)
|
||||
.build(),
|
||||
anchor,
|
||||
)
|
||||
}
|
||||
|
||||
fun openReader(intent: ReaderIntent, anchor: View? = null) {
|
||||
startActivity(intent.intent, anchor?.let { view -> scaleUpActivityOptionsOf(view) })
|
||||
}
|
||||
|
||||
fun openAlternatives(manga: Manga) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, AlternativesActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openRelated(manga: Manga) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), RelatedMangaActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openImage(url: String, source: MangaSource?, anchor: View? = null) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), ImageActivity::class.java)
|
||||
.setData(url.toUri())
|
||||
.putExtra(KEY_SOURCE, source?.name),
|
||||
anchor?.let { scaleUpActivityOptionsOf(it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun openBookmarks() = startActivity(AllBookmarksActivity::class.java)
|
||||
|
||||
fun openAppUpdate() = startActivity(AppUpdateActivity::class.java)
|
||||
|
||||
fun openSuggestions() {
|
||||
startActivity(suggestionsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourcesCatalog() = startActivity(SourcesCatalogActivity::class.java)
|
||||
|
||||
fun openDownloads() = startActivity(DownloadsActivity::class.java)
|
||||
|
||||
fun openDirectoriesSettings() = startActivity(MangaDirectoriesActivity::class.java)
|
||||
|
||||
fun openBrowser(url: String, source: MangaSource?, title: String?) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, BrowserActivity::class.java)
|
||||
.setData(url.toUri())
|
||||
.putExtra(KEY_TITLE, title)
|
||||
.putExtra(KEY_SOURCE, source?.name),
|
||||
)
|
||||
}
|
||||
|
||||
fun openColorFilterConfig(manga: Manga, page: MangaPage) {
|
||||
startActivity(
|
||||
Intent(contextOrNull(), ColorFilterConfigActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga))
|
||||
.putExtra(KEY_PAGES, ParcelableMangaPage(page)),
|
||||
)
|
||||
}
|
||||
|
||||
fun openHistory() = startActivity(HistoryActivity::class.java)
|
||||
|
||||
fun openFavorites() = startActivity(FavouritesActivity::class.java)
|
||||
|
||||
fun openFavorites(category: FavouriteCategory) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, FavouritesActivity::class.java)
|
||||
.putExtra(KEY_ID, category.id)
|
||||
.putExtra(KEY_TITLE, category.title),
|
||||
)
|
||||
}
|
||||
|
||||
fun openFavoriteCategories() = startActivity(FavouriteCategoriesActivity::class.java)
|
||||
|
||||
fun openFavoriteCategoryEdit(categoryId: Long) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, FavouritesCategoryEditActivity::class.java)
|
||||
.putExtra(KEY_ID, categoryId),
|
||||
)
|
||||
}
|
||||
|
||||
fun openFavoriteCategoryCreate() = openFavoriteCategoryEdit(FavouritesCategoryEditActivity.NO_ID)
|
||||
|
||||
fun openMangaUpdates() {
|
||||
startActivity(mangaUpdatesIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSettings() = startActivity(SettingsActivity::class.java)
|
||||
|
||||
fun openReaderSettings() {
|
||||
startActivity(readerSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openProxySettings() {
|
||||
startActivity(proxySettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openDownloadsSetting() {
|
||||
startActivity(downloadsSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourceSettings(source: MangaSource) {
|
||||
startActivity(sourceSettingsIntent(contextOrNull() ?: return, source))
|
||||
}
|
||||
|
||||
fun openSuggestionsSettings() {
|
||||
startActivity(suggestionsSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openSourcesSettings() {
|
||||
startActivity(sourcesSettingsIntent(contextOrNull() ?: return))
|
||||
}
|
||||
|
||||
fun openReaderTapGridSettings() = startActivity(ReaderTapGridConfigActivity::class.java)
|
||||
|
||||
fun openScrobblerSettings(scrobbler: ScrobblerService) {
|
||||
startActivity(
|
||||
Intent(contextOrNull() ?: return, ScrobblerConfigActivity::class.java)
|
||||
.putExtra(KEY_ID, scrobbler.id),
|
||||
)
|
||||
}
|
||||
|
||||
fun openSourceAuth(source: MangaSource) {
|
||||
startActivity(sourceAuthIntent(contextOrNull() ?: return, source))
|
||||
}
|
||||
|
||||
fun openManageSources() {
|
||||
startActivity(
|
||||
manageSourcesIntent(contextOrNull() ?: return),
|
||||
)
|
||||
}
|
||||
|
||||
fun openStatistic() = startActivity(StatsActivity::class.java)
|
||||
|
||||
@CheckResult
|
||||
fun openExternalBrowser(url: String, chooserTitle: CharSequence? = null): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = url.toUriOrNull() ?: return false
|
||||
return startActivitySafe(
|
||||
if (!chooserTitle.isNullOrEmpty()) {
|
||||
Intent.createChooser(intent, chooserTitle)
|
||||
} else {
|
||||
intent
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
fun openSystemSyncSettings(account: Account): Boolean {
|
||||
val args = Bundle(1)
|
||||
args.putParcelable(ACCOUNT_KEY, account)
|
||||
val intent = Intent(ACTION_ACCOUNT_SYNC_SETTINGS)
|
||||
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args)
|
||||
return startActivitySafe(intent)
|
||||
}
|
||||
|
||||
/** Dialogs **/
|
||||
|
||||
fun showDownloadDialog(manga: Manga, snackbarHost: View?) = showDownloadDialog(setOf(manga), snackbarHost)
|
||||
|
||||
fun showDownloadDialog(manga: Collection<Manga>, snackbarHost: View?) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val fm = getFragmentManager() ?: return
|
||||
if (snackbarHost != null) {
|
||||
getLifecycleOwner()?.let { lifecycleOwner ->
|
||||
DownloadDialogFragment.registerCallback(fm, lifecycleOwner, snackbarHost)
|
||||
}
|
||||
} else {
|
||||
DownloadDialogFragment.unregisterCallback(fm)
|
||||
}
|
||||
DownloadDialogFragment().withArgs(1) {
|
||||
putParcelableArray(KEY_MANGA, manga.mapToArray { ParcelableManga(it, withDescription = false) })
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showLocalInfoDialog(manga: Manga) {
|
||||
LocalInfoDialog().withArgs(1) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showDirectorySelectDialog() {
|
||||
MangaDirectorySelectDialog().showDistinct()
|
||||
}
|
||||
|
||||
fun showFavoriteDialog(manga: Manga) = showFavoriteDialog(setOf(manga))
|
||||
|
||||
fun showFavoriteDialog(manga: Collection<Manga>) {
|
||||
if (manga.isEmpty()) {
|
||||
return
|
||||
}
|
||||
FavoriteDialog().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withDescription = false) },
|
||||
)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showTagDialog(tag: MangaTag) {
|
||||
buildAlertDialog(contextOrNull() ?: return) {
|
||||
setTitle(tag.title)
|
||||
setItems(
|
||||
arrayOf(
|
||||
context.getString(R.string.search_on_s, tag.source.getTitle(context)),
|
||||
context.getString(R.string.search_everywhere),
|
||||
),
|
||||
) { _, which ->
|
||||
when (which) {
|
||||
0 -> openList(tag)
|
||||
1 -> openSearch(tag.title, SearchKind.TAG)
|
||||
}
|
||||
}
|
||||
setNegativeButton(R.string.close, null)
|
||||
setCancelable(true)
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showAuthorDialog(author: String, source: MangaSource) {
|
||||
buildAlertDialog(contextOrNull() ?: return) {
|
||||
setTitle(author)
|
||||
setItems(
|
||||
arrayOf(
|
||||
context.getString(R.string.search_on_s, source.getTitle(context)),
|
||||
context.getString(R.string.search_everywhere),
|
||||
),
|
||||
) { _, which ->
|
||||
when (which) {
|
||||
0 -> openList(source, MangaListFilter(author = author), null)
|
||||
1 -> openSearch(author, SearchKind.AUTHOR)
|
||||
}
|
||||
}
|
||||
setNegativeButton(R.string.close, null)
|
||||
setCancelable(true)
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showErrorDialog(error: Throwable, url: String? = null) {
|
||||
ErrorDetailsDialog().withArgs(2) {
|
||||
putSerializable(KEY_ERROR, error)
|
||||
putString(KEY_URL, url)
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showBackupRestoreDialog(fileUri: Uri) {
|
||||
RestoreDialogFragment().withArgs(1) {
|
||||
putString(KEY_FILE, fileUri.toString())
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showBackupCreateDialog() {
|
||||
BackupDialogFragment().show()
|
||||
}
|
||||
|
||||
fun showImportDialog() {
|
||||
ImportDialogFragment().showDistinct()
|
||||
}
|
||||
|
||||
fun showFilterSheet(): Boolean = if (isFilterSupported()) {
|
||||
FilterSheetFragment().showDistinct()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
fun showTagsCatalogSheet(excludeMode: Boolean) {
|
||||
if (!isFilterSupported()) {
|
||||
return
|
||||
}
|
||||
TagsCatalogSheet().withArgs(1) {
|
||||
putBoolean(KEY_EXCLUDE, excludeMode)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showListConfigSheet(section: ListConfigSection) {
|
||||
ListConfigBottomSheet().withArgs(1) {
|
||||
putParcelable(KEY_LIST_SECTION, section)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showStatisticSheet(manga: Manga) {
|
||||
MangaStatsSheet().withArgs(1) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showReaderConfigSheet(mode: ReaderMode) {
|
||||
ReaderConfigSheet().withArgs(1) {
|
||||
putInt(KEY_READER_MODE, mode.id)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showWelcomeSheet() {
|
||||
WelcomeSheet().showDistinct()
|
||||
}
|
||||
|
||||
fun showChapterPagesSheet() {
|
||||
ChaptersPagesSheet().showDistinct()
|
||||
}
|
||||
|
||||
fun showChapterPagesSheet(defaultTab: Int) {
|
||||
ChaptersPagesSheet().withArgs(1) {
|
||||
putInt(KEY_TAB, defaultTab)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showScrobblingSelectorSheet(manga: Manga, scrobblerService: ScrobblerService?) {
|
||||
ScrobblingSelectorSheet().withArgs(2) {
|
||||
putParcelable(KEY_MANGA, ParcelableManga(manga))
|
||||
if (scrobblerService != null) {
|
||||
putInt(KEY_ID, scrobblerService.id)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
fun showScrobblingInfoSheet(index: Int) {
|
||||
ScrobblingInfoSheet().withArgs(1) {
|
||||
putInt(KEY_INDEX, index)
|
||||
}.showDistinct()
|
||||
}
|
||||
|
||||
fun showTrackerCategoriesConfigSheet() {
|
||||
TrackerCategoriesConfigSheet().showDistinct()
|
||||
}
|
||||
|
||||
fun askForDownloadOverMeteredNetwork(onConfirmed: (allow: Boolean) -> Unit) {
|
||||
val context = contextOrNull() ?: return
|
||||
when (settings.allowDownloadOnMeteredNetwork) {
|
||||
TriStateOption.ENABLED -> onConfirmed(true)
|
||||
TriStateOption.DISABLED -> onConfirmed(false)
|
||||
TriStateOption.ASK -> {
|
||||
if (!context.connectivityManager.isActiveNetworkMetered) {
|
||||
onConfirmed(true)
|
||||
return
|
||||
}
|
||||
val listener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
settings.allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
settings.allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
|
||||
onConfirmed(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
BigButtonsAlertDialog.Builder(context)
|
||||
.setIcon(R.drawable.ic_network_cellular)
|
||||
.setTitle(R.string.download_cellular_confirm)
|
||||
.setPositiveButton(R.string.allow_always, listener)
|
||||
.setNeutralButton(R.string.allow_once, listener)
|
||||
.setNegativeButton(R.string.dont_allow, listener)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Public utils **/
|
||||
|
||||
fun isFilterSupported(): Boolean = when {
|
||||
fragment != null -> fragment.activity is FilterCoordinator.Owner
|
||||
activity != null -> activity is FilterCoordinator.Owner
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun isChapterPagesSheetShown(): Boolean {
|
||||
val sheet = getFragmentManager()?.findFragmentByTag(fragmentTag<ChaptersPagesSheet>()) as? ChaptersPagesSheet
|
||||
return sheet?.dialog?.isShowing == true
|
||||
}
|
||||
|
||||
fun closeWelcomeSheet(): Boolean {
|
||||
val tag = fragmentTag<WelcomeSheet>()
|
||||
val sheet = fragment?.findFragmentByTagRecursive(tag)
|
||||
?: activity?.supportFragmentManager?.findFragmentByTag(tag)
|
||||
?: return false
|
||||
return if (sheet is WelcomeSheet) {
|
||||
sheet.dismissAllowingStateLoss()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/** Private utils **/
|
||||
|
||||
private fun startActivity(intent: Intent, options: Bundle? = null) {
|
||||
fragment?.startActivity(intent, options)
|
||||
?: activity?.startActivity(intent, options)
|
||||
}
|
||||
|
||||
private fun startActivitySafe(intent: Intent): Boolean = try {
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun startActivity(activityClass: Class<out Activity>) {
|
||||
startActivity(Intent(contextOrNull() ?: return, activityClass))
|
||||
}
|
||||
|
||||
private fun getFragmentManager(): FragmentManager? {
|
||||
return fragment?.childFragmentManager ?: activity?.supportFragmentManager
|
||||
}
|
||||
|
||||
@UiContext
|
||||
private fun contextOrNull(): Context? = activity ?: fragment?.context
|
||||
|
||||
private fun getLifecycleOwner(): LifecycleOwner? = activity ?: fragment?.viewLifecycleOwner
|
||||
|
||||
private fun DialogFragment.showDistinct(): Boolean {
|
||||
val fm = this@AppRouter.getFragmentManager() ?: return false
|
||||
val tag = javaClass.fragmentTag()
|
||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||
return false
|
||||
}
|
||||
show(fm, tag)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun DialogFragment.show() {
|
||||
show(
|
||||
this@AppRouter.getFragmentManager() ?: return,
|
||||
javaClass.fragmentTag(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Fragment.findFragmentByTagRecursive(fragmentTag: String): Fragment? {
|
||||
childFragmentManager.findFragmentByTag(fragmentTag)?.let {
|
||||
return it
|
||||
}
|
||||
val parent = parentFragment
|
||||
return if (parent != null) {
|
||||
parent.findFragmentByTagRecursive(fragmentTag)
|
||||
} else {
|
||||
parentFragmentManager.findFragmentByTag(fragmentTag)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(view: View): AppRouter? = runCatching {
|
||||
AppRouter(view.findFragment<Fragment>())
|
||||
}.getOrElse {
|
||||
(view.context.findActivity() as? FragmentActivity)?.let(::AppRouter)
|
||||
}
|
||||
|
||||
fun detailsIntent(context: Context, manga: Manga) = Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(KEY_MANGA, ParcelableManga(manga))
|
||||
|
||||
fun detailsIntent(context: Context, mangaId: Long) = Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(KEY_ID, mangaId)
|
||||
|
||||
fun listIntent(context: Context, source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?): Intent =
|
||||
Intent(context, MangaListActivity::class.java)
|
||||
.setAction(ACTION_MANGA_EXPLORE)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
.apply {
|
||||
if (!filter.isNullOrEmpty()) {
|
||||
putExtra(KEY_FILTER, ParcelableMangaListFilter(filter))
|
||||
}
|
||||
if (sortOrder != null) {
|
||||
putExtra(KEY_SORT_ORDER, sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
fun cloudFlareResolveIntent(context: Context, exception: CloudFlareProtectedException): Intent =
|
||||
Intent(context, CloudFlareActivity::class.java).apply {
|
||||
data = exception.url.toUri()
|
||||
putExtra(KEY_SOURCE, exception.source?.name)
|
||||
exception.headers[CommonHeaders.USER_AGENT]?.let {
|
||||
putExtra(KEY_USER_AGENT, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
|
||||
|
||||
fun homeIntent(context: Context) = Intent(context, MainActivity::class.java)
|
||||
|
||||
fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
|
||||
|
||||
fun readerSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_READER)
|
||||
|
||||
fun suggestionsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SUGGESTIONS)
|
||||
|
||||
fun trackerSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_TRACKER)
|
||||
|
||||
fun proxySettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_PROXY)
|
||||
|
||||
fun historySettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_HISTORY)
|
||||
|
||||
fun sourcesSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCES)
|
||||
|
||||
fun manageSourcesIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_SOURCES)
|
||||
|
||||
fun downloadsSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_DOWNLOADS)
|
||||
|
||||
fun sourceSettingsIntent(context: Context, source: MangaSource): Intent = when (source) {
|
||||
is MangaSourceInfo -> sourceSettingsIntent(context, source.mangaSource)
|
||||
is ExternalMangaSource -> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", source.packageName, null))
|
||||
|
||||
else -> Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
}
|
||||
|
||||
fun sourceAuthIntent(context: Context, source: MangaSource): Intent {
|
||||
return Intent(context, SourceAuthActivity::class.java)
|
||||
.putExtra(KEY_SOURCE, source.name)
|
||||
}
|
||||
|
||||
const val KEY_DATA = "data"
|
||||
const val KEY_ENTRIES = "entries"
|
||||
const val KEY_ERROR = "error"
|
||||
const val KEY_EXCLUDE = "exclude"
|
||||
const val KEY_FILE = "file"
|
||||
const val KEY_FILTER = "filter"
|
||||
const val KEY_ID = "id"
|
||||
const val KEY_INDEX = "index"
|
||||
const val KEY_KIND = "kind"
|
||||
const val KEY_LIST_SECTION = "list_section"
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_MANGA_LIST = "manga_list"
|
||||
const val KEY_PAGES = "pages"
|
||||
const val KEY_QUERY = "query"
|
||||
const val KEY_READER_MODE = "reader_mode"
|
||||
const val KEY_SORT_ORDER = "sort_order"
|
||||
const val KEY_SOURCE = "source"
|
||||
const val KEY_TAB = "tab"
|
||||
const val KEY_TITLE = "title"
|
||||
const val KEY_URL = "url"
|
||||
const val KEY_USER_AGENT = "user_agent"
|
||||
|
||||
const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
|
||||
const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
|
||||
const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
|
||||
const val ACTION_PROXY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PROXY"
|
||||
const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
|
||||
const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
|
||||
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
|
||||
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
||||
|
||||
private const val ACCOUNT_KEY = "account"
|
||||
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"
|
||||
private const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
|
||||
|
||||
private fun Class<out Fragment>.fragmentTag() = name // TODO
|
||||
|
||||
private inline fun <reified F : Fragment> fragmentTag() = F::class.java.fragmentTag()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AppRouterEntryPoint {
|
||||
|
||||
val settings: AppSettings
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.app.ActivityOptions
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
|
||||
inline val FragmentActivity.router: AppRouter
|
||||
get() = AppRouter(this)
|
||||
|
||||
inline val Fragment.router: AppRouter
|
||||
get() = AppRouter(this)
|
||||
|
||||
tailrec fun Fragment.dismissParentDialog(): Boolean {
|
||||
return when (val parent = parentFragment) {
|
||||
null -> return false
|
||||
is DialogFragment -> {
|
||||
parent.dismiss()
|
||||
true
|
||||
}
|
||||
|
||||
else -> parent.dismissParentDialog()
|
||||
}
|
||||
}
|
||||
|
||||
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
|
||||
ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.width,
|
||||
view.height,
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
|
||||
@JvmInline
|
||||
value class ReaderIntent private constructor(
|
||||
val intent: Intent,
|
||||
) {
|
||||
|
||||
class Builder(context: Context) {
|
||||
|
||||
private val intent = Intent(context, ReaderActivity::class.java)
|
||||
.setAction(ACTION_MANGA_READ)
|
||||
|
||||
fun manga(manga: Manga) = apply {
|
||||
intent.putExtra(AppRouter.KEY_MANGA, ParcelableManga(manga))
|
||||
}
|
||||
|
||||
fun mangaId(mangaId: Long) = apply {
|
||||
intent.putExtra(AppRouter.KEY_ID, mangaId)
|
||||
}
|
||||
|
||||
fun incognito(incognito: Boolean) = apply {
|
||||
intent.putExtra(EXTRA_INCOGNITO, incognito)
|
||||
}
|
||||
|
||||
fun branch(branch: String?) = apply {
|
||||
intent.putExtra(EXTRA_BRANCH, branch)
|
||||
}
|
||||
|
||||
fun state(state: ReaderState?) = apply {
|
||||
intent.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
fun bookmark(bookmark: Bookmark) = manga(
|
||||
bookmark.manga,
|
||||
).state(
|
||||
ReaderState(
|
||||
chapterId = bookmark.chapterId,
|
||||
page = bookmark.page,
|
||||
scroll = bookmark.scroll,
|
||||
),
|
||||
)
|
||||
|
||||
fun build() = ReaderIntent(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
|
||||
const val EXTRA_STATE = "state"
|
||||
const val EXTRA_BRANCH = "branch"
|
||||
const val EXTRA_INCOGNITO = "incognito"
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.nav.ReaderIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -38,6 +36,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -155,10 +155,9 @@ class AppShortcutManager @Inject constructor(
|
||||
.setIcon(icon)
|
||||
.setLongLived(true)
|
||||
.setIntent(
|
||||
ReaderIntent.Builder(context)
|
||||
ReaderActivity.IntentBuilder(context)
|
||||
.mangaId(manga.id)
|
||||
.build()
|
||||
.intent,
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
@@ -182,7 +181,7 @@ class AppShortcutManager @Inject constructor(
|
||||
.setLongLabel(title)
|
||||
.setIcon(icon)
|
||||
.setLongLived(true)
|
||||
.setIntent(AppRouter.listIntent(context, source, null, null))
|
||||
.setIntent(MangaListActivity.newIntent(context, source, null))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -14,8 +11,8 @@ import javax.inject.Singleton
|
||||
class AppValidator @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
@SuppressLint("InlinedApi")
|
||||
val isOriginalApp = suspendLazy(Dispatchers.Default) {
|
||||
@Suppress("NewApi")
|
||||
val isOriginalApp by lazy {
|
||||
val certificates = mapOf(CERT_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256)
|
||||
PackageInfoCompat.hasSignatures(context.packageManager, context.packageName, certificates, false)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class NetworkState(
|
||||
return true
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activeNetwork?.let { isOnline(it) } == true
|
||||
activeNetwork?.let { isOnline(it) } ?: false
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activeNetworkInfo?.isConnected == true
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
|
||||
// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
|
||||
class OpenDocumentTreeHelper(
|
||||
activityResultCaller: ActivityResultCaller,
|
||||
flags: Int,
|
||||
callback: ActivityResultCallback<Uri?>
|
||||
) : ActivityResultLauncher<Uri?>() {
|
||||
|
||||
constructor(activityResultCaller: ActivityResultCaller, callback: ActivityResultCallback<Uri?>) : this(
|
||||
activityResultCaller,
|
||||
0,
|
||||
callback,
|
||||
)
|
||||
|
||||
private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult(
|
||||
contract = OpenDocumentTreeContractLegacy(flags),
|
||||
callback = callback,
|
||||
)
|
||||
|
||||
override fun launch(input: Uri?, options: ActivityOptionsCompat?) {
|
||||
if (pickFileTreeLauncherQ == null) {
|
||||
pickFileTreeLauncherLegacy.launch(input, options)
|
||||
return
|
||||
}
|
||||
try {
|
||||
pickFileTreeLauncherQ.launch(input, options)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
pickFileTreeLauncherLegacy.launch(input, options)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
pickFileTreeLauncherQ?.unregister()
|
||||
pickFileTreeLauncherLegacy.unregister()
|
||||
}
|
||||
|
||||
override val contract: ActivityResultContract<Uri?, *>
|
||||
get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract
|
||||
|
||||
private open class OpenDocumentTreeContractLegacy(
|
||||
private val flags: Int,
|
||||
) : ActivityResultContracts.OpenDocumentTree() {
|
||||
|
||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
intent.addFlags(flags)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private class OpenDocumentTreeContractQ(
|
||||
private val flags: Int,
|
||||
) : OpenDocumentTreeContractLegacy(flags) {
|
||||
|
||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||
val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager)
|
||||
?.primaryStorageVolume
|
||||
?.createOpenDocumentTreeIntent()
|
||||
if (intent == null) { // fallback
|
||||
return super.createIntent(context, input)
|
||||
}
|
||||
intent.addFlags(flags)
|
||||
if (input != null) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object RomCompat {
|
||||
|
||||
val isMiui = suspendLazy<Boolean>(Dispatchers.IO) {
|
||||
getProp("ro.miui.ui.version.name").isNotEmpty()
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private fun getProp(propName: String) = Runtime.getRuntime().exec("getprop $propName").inputStream.use {
|
||||
it.reader().use(InputStreamReader::readText).trim()
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,22 @@ package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
|
||||
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
|
||||
import java.util.EnumSet
|
||||
|
||||
/**
|
||||
* This parser is just for parser development, it should not be used in releases
|
||||
*/
|
||||
class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.DUMMY) {
|
||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("localhost")
|
||||
@@ -25,14 +25,14 @@ class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, Ma
|
||||
override val availableSortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
override val searchQueryCapabilities: MangaSearchQueryCapabilities
|
||||
get() = MangaSearchQueryCapabilities()
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||
override val filterCapabilities: MangaListFilterCapabilities
|
||||
get() = MangaListFilterCapabilities()
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
|
||||
|
||||
override suspend fun getList(query: MangaSearchQuery): List<Manga> = stub(null)
|
||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||
|
||||
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> = stub(null)
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.nav.MangaIntent
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -29,7 +27,6 @@ import javax.inject.Provider
|
||||
class MangaDataRepository @Inject constructor(
|
||||
private val db: MangaDatabase,
|
||||
private val resolverProvider: Provider<MangaLinkResolver>,
|
||||
private val appShortcutManagerProvider: Provider<AppShortcutManager>,
|
||||
) {
|
||||
|
||||
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
|
||||
@@ -48,8 +45,8 @@ class MangaDataRepository @Inject constructor(
|
||||
entity.copy(
|
||||
cfBrightness = colorFilter?.brightness ?: 0f,
|
||||
cfContrast = colorFilter?.contrast ?: 0f,
|
||||
cfInvert = colorFilter?.isInverted == true,
|
||||
cfGrayscale = colorFilter?.isGrayscale == true,
|
||||
cfInvert = colorFilter?.isInverted ?: false,
|
||||
cfGrayscale = colorFilter?.isGrayscale ?: false,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -73,13 +70,8 @@ class MangaDataRepository @Inject constructor(
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
suspend fun findMangaById(mangaId: Long, withChapters: Boolean): Manga? {
|
||||
val chapters = if (withChapters) {
|
||||
db.getChaptersDao().findAll(mangaId).takeUnless { it.isEmpty() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return db.getMangaDao().find(mangaId)?.toManga(chapters)
|
||||
suspend fun findMangaById(mangaId: Long): Manga? {
|
||||
return db.getMangaDao().find(mangaId)?.toManga()
|
||||
}
|
||||
|
||||
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
|
||||
@@ -88,7 +80,7 @@ class MangaDataRepository @Inject constructor(
|
||||
|
||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||
intent.manga != null -> intent.manga
|
||||
intent.mangaId != 0L -> findMangaById(intent.mangaId, true)
|
||||
intent.mangaId != 0L -> findMangaById(intent.mangaId)
|
||||
intent.uri != null -> resolverProvider.get().resolve(intent.uri)
|
||||
else -> null
|
||||
}
|
||||
@@ -105,26 +97,10 @@ class MangaDataRepository @Inject constructor(
|
||||
val tags = manga.tags.toEntities()
|
||||
db.getTagsDao().upsert(tags)
|
||||
db.getMangaDao().upsert(manga.toEntity(), tags)
|
||||
if (!manga.isLocal) {
|
||||
manga.chapters?.let { chapters ->
|
||||
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateChapters(manga: Manga) {
|
||||
val chapters = manga.chapters
|
||||
if (!chapters.isNullOrEmpty() && manga.id in db.getMangaDao()) {
|
||||
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun gcChaptersCache() {
|
||||
db.getChaptersDao().gc()
|
||||
}
|
||||
|
||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
||||
return db.getTagsDao().findTags(source.name).toMangaTags()
|
||||
}
|
||||
@@ -138,14 +114,6 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cleanupDatabase() {
|
||||
db.withTransaction {
|
||||
gcChaptersCache()
|
||||
val idsFromShortcuts = appShortcutManagerProvider.get().getMangaShortcuts()
|
||||
db.getMangaDao().cleanup(idsFromShortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
||||
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) {
|
||||
ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.nav
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_ID
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_MANGA
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -26,7 +25,7 @@ class MangaIntent private constructor(
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
manga = savedStateHandle.get<ParcelableManga>(KEY_MANGA)?.manga,
|
||||
id = savedStateHandle[KEY_ID] ?: ID_NONE,
|
||||
uri = savedStateHandle[AppRouter.KEY_DATA],
|
||||
uri = savedStateHandle[BaseActivity.EXTRA_DATA],
|
||||
)
|
||||
|
||||
constructor(args: Bundle?) : this(
|
||||
@@ -42,6 +41,9 @@ class MangaIntent private constructor(
|
||||
|
||||
const val ID_NONE = 0L
|
||||
|
||||
const val KEY_MANGA = "manga"
|
||||
const val KEY_ID = "id"
|
||||
|
||||
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import dagger.Reusable
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -110,11 +109,4 @@ class MangaLinkResolver @Inject constructor(
|
||||
chapters = null,
|
||||
source = source,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
fun isValidLink(str: String): Boolean {
|
||||
return str.isHttpUrl() || str.startsWith("kotatsu://", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.core.util.ext.configureForParser
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.core.util.ext.toMimeType
|
||||
import org.koitharu.kotatsu.core.util.ext.use
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||
@@ -79,14 +78,13 @@ class MangaLoaderContextImpl @Inject constructor(
|
||||
|
||||
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
|
||||
return response.map { body ->
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType()?.toMimeType(), isMutable = true)
|
||||
.use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap ->
|
||||
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
|
||||
Buffer().also {
|
||||
result.compressTo(it.outputStream())
|
||||
}.asResponseBody("image/jpeg".toMediaType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExternalMangaRepository(
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = capabilities?.availableSortOrders?.firstOrNull() ?: SortOrder.ALPHABETICAL
|
||||
set(value) = Unit
|
||||
set(_) = Unit
|
||||
|
||||
override suspend fun getFilterOptions(): MangaListFilterOptions = filterOptions.get()
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ class ExternalPluginContentSource(
|
||||
do {
|
||||
result += MangaChapter(
|
||||
id = cursor.getLong(COLUMN_ID),
|
||||
title = cursor.getStringOrNull(COLUMN_NAME),
|
||||
name = cursor.getString(COLUMN_NAME),
|
||||
number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f),
|
||||
volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0),
|
||||
url = cursor.getString(COLUMN_URL),
|
||||
@@ -252,7 +252,7 @@ class ExternalPluginContentSource(
|
||||
publicUrl = getString(COLUMN_PUBLIC_URL),
|
||||
rating = getFloat(COLUMN_RATING),
|
||||
isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false),
|
||||
coverUrl = getStringOrNull(COLUMN_COVER_URL),
|
||||
coverUrl = getString(COLUMN_COVER_URL),
|
||||
tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet {
|
||||
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
||||
MangaTag(key = parts.first, title = parts.second, source = source)
|
||||
|
||||
@@ -20,7 +20,7 @@ class ExternalPluginCursor(private val source: ExternalMangaSource, cursor: Curs
|
||||
return when {
|
||||
columnIndex < 0 -> null
|
||||
isNull(columnIndex) -> null
|
||||
else -> getString(columnIndex).takeUnless { it == "null" }
|
||||
else -> getString(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,12 @@ import androidx.core.os.LocaleListCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.getEnumValue
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.putAll
|
||||
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
@@ -45,7 +44,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val connectivityManager = context.connectivityManager
|
||||
private val mangaListBadgesDefault = ArraySet(context.resources.getStringArray(R.array.values_list_badges))
|
||||
|
||||
var listMode: ListMode
|
||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||
@@ -143,11 +141,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderOptimizationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false)
|
||||
|
||||
val readerControls: Set<ReaderControl>
|
||||
get() = prefs.getStringSet(KEY_READER_CONTROLS, null)?.mapNotNullTo(EnumSet.noneOf(ReaderControl::class.java)) {
|
||||
ReaderControl.entries.find(it)
|
||||
} ?: ReaderControl.DEFAULT
|
||||
|
||||
val isOfflineCheckDisabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false)
|
||||
|
||||
@@ -306,10 +299,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
||||
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
||||
|
||||
var isAllSourcesEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_ENABLED_ALL, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_ENABLED_ALL, value) }
|
||||
|
||||
val isPagesNumbersEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||
|
||||
@@ -374,8 +363,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderBarEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
||||
|
||||
val isReaderBarTransparent: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_BAR_TRANSPARENT, true)
|
||||
val isReaderSliderEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||
|
||||
val isReaderKeepScreenOn: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
|
||||
@@ -500,12 +489,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
|
||||
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
|
||||
|
||||
val isBackupTelegramUploadEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_BACKUP_TG_ENABLED, false)
|
||||
|
||||
val backupTelegramChatId: String?
|
||||
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)?.nullIfEmpty()
|
||||
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
|
||||
@@ -548,15 +531,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) }
|
||||
}
|
||||
|
||||
fun getMangaListBadges(): Int {
|
||||
val raw = prefs.getStringSet(KEY_MANGA_LIST_BADGES, mangaListBadgesDefault).orEmpty()
|
||||
var result = 0
|
||||
for (item in raw) {
|
||||
result = result or item.toInt()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
@@ -569,7 +543,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
fun getAllValues(): Map<String, *> = prefs.all
|
||||
|
||||
fun upsertAll(m: Map<String, *>) = prefs.edit { putAll(m) }
|
||||
fun upsertAll(m: Map<String, *>) {
|
||||
prefs.edit {
|
||||
m.forEach { e ->
|
||||
when (val v = e.value) {
|
||||
is Boolean -> putBoolean(e.key, v)
|
||||
is Int -> putInt(e.key, v)
|
||||
is Long -> putLong(e.key, v)
|
||||
is Float -> putFloat(e.key, v)
|
||||
is String -> putString(e.key, v)
|
||||
is JSONArray -> putStringSet(e.key, v.toStringSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBackgroundNetworkRestricted(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -579,6 +566,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONArray.toStringSet(): Set<String> {
|
||||
val len = length()
|
||||
val result = ArraySet<String>(len)
|
||||
for (i in 0 until len) {
|
||||
result.add(getString(i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TRACK_HISTORY = "history"
|
||||
@@ -625,7 +621,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
|
||||
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
|
||||
const val KEY_READER_ANIMATION = "reader_animation2"
|
||||
const val KEY_READER_CONTROLS = "reader_controls"
|
||||
const val KEY_READER_MODE = "reader_mode"
|
||||
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
|
||||
const val KEY_READER_CROP = "reader_crop"
|
||||
@@ -669,7 +664,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SYNC = "sync"
|
||||
const val KEY_SYNC_SETTINGS = "sync_settings"
|
||||
const val KEY_READER_BAR = "reader_bar"
|
||||
const val KEY_READER_BAR_TRANSPARENT = "reader_bar_transparent"
|
||||
const val KEY_READER_SLIDER = "reader_slider"
|
||||
const val KEY_READER_BACKGROUND = "reader_background"
|
||||
const val KEY_READER_SCREEN_ON = "reader_screen_on"
|
||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||
@@ -720,16 +715,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_FEED_HEADER = "feed_header"
|
||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||
const val KEY_SOURCES_VERSION = "sources_version"
|
||||
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
|
||||
const val KEY_QUICK_FILTER = "quick_filter"
|
||||
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
|
||||
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"
|
||||
const val KEY_MANGA_LIST_BADGES = "manga_list_badges"
|
||||
|
||||
// keys for non-persistent preferences
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||
const val KEY_TRACKER_DEBUG = "tracker_debug"
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_LINK_WEBLATE = "about_app_translation"
|
||||
const val KEY_LINK_TELEGRAM = "about_telegram"
|
||||
const val KEY_LINK_GITHUB = "about_github"
|
||||
@@ -737,10 +729,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PROXY_TEST = "proxy_test"
|
||||
const val KEY_OPEN_BROWSER = "open_browser"
|
||||
const val KEY_HANDLE_LINKS = "handle_links"
|
||||
const val KEY_BACKUP_TG_OPEN = "backup_periodic_tg_open"
|
||||
const val KEY_BACKUP_TG_TEST = "backup_periodic_tg_test"
|
||||
const val KEY_CLEAR_MANGA_DATA = "manga_data_clear"
|
||||
const val KEY_STORAGE_USAGE = "storage_usage"
|
||||
|
||||
// old keys are for migration only
|
||||
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.isNightMode
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@Keep
|
||||
@@ -15,7 +13,7 @@ enum class ReaderBackground {
|
||||
|
||||
DEFAULT, LIGHT, DARK, WHITE, BLACK;
|
||||
|
||||
fun resolve(context: Context): Drawable? = when (this) {
|
||||
fun resolve(context: Context) = when (this) {
|
||||
DEFAULT -> context.getThemeDrawable(android.R.attr.windowBackground)
|
||||
LIGHT -> ContextThemeWrapper(context, materialR.style.ThemeOverlay_Material3_Light)
|
||||
.getThemeDrawable(android.R.attr.windowBackground)
|
||||
@@ -26,14 +24,4 @@ enum class ReaderBackground {
|
||||
WHITE -> ContextCompat.getColor(context, android.R.color.white).toDrawable()
|
||||
BLACK -> ContextCompat.getColor(context, android.R.color.black).toDrawable()
|
||||
}
|
||||
|
||||
fun isLight(context: Context): Boolean = when (this) {
|
||||
DEFAULT -> !context.resources.isNightMode
|
||||
|
||||
LIGHT,
|
||||
WHITE -> true
|
||||
|
||||
DARK,
|
||||
BLACK -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import java.util.EnumSet
|
||||
|
||||
enum class ReaderControl {
|
||||
|
||||
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET, SCREEN_ROTATION, SAVE_PAGE;
|
||||
|
||||
companion object {
|
||||
|
||||
val DEFAULT: Set<ReaderControl> = EnumSet.of(
|
||||
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
||||
val isSlowdownEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SLOWDOWN, false)
|
||||
|
||||
val isCaptchaNotificationsDisabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_NO_CAPTCHA, false)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(key: ConfigKey<T>): T {
|
||||
return when (key) {
|
||||
@@ -68,6 +65,5 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
||||
|
||||
const val KEY_SORT_ORDER = "sort_order"
|
||||
const val KEY_SLOWDOWN = "slowdown"
|
||||
const val KEY_NO_CAPTCHA = "no_captcha"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
@@ -23,15 +21,16 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
|
||||
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
|
||||
|
||||
abstract class BaseActivity<B : ViewBinding> :
|
||||
AppCompatActivity(),
|
||||
ExceptionResolver.Host,
|
||||
ScreenshotPolicyHelper.ContentContainer {
|
||||
ScreenshotPolicyHelper.ContentContainer,
|
||||
WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
private var isAmoledTheme = false
|
||||
|
||||
@@ -41,20 +40,16 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
@JvmField
|
||||
val actionModeDelegate = ActionModeDelegate()
|
||||
|
||||
private lateinit var entryPoint: BaseActivityEntryPoint
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(newBase.applicationContext)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
AppCompatDelegate.setApplicationLocales(entryPoint.settings.appLocales)
|
||||
}
|
||||
super.attachBaseContext(newBase)
|
||||
}
|
||||
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(this)
|
||||
val settings = entryPoint.settings
|
||||
isAmoledTheme = settings.isAmoledTheme
|
||||
setTheme(settings.colorScheme.styleResId)
|
||||
@@ -63,8 +58,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
putDataToExtras(intent)
|
||||
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
insetsDelegate.handleImeInsets = true
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
@@ -98,6 +95,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
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 onSupportNavigateUp(): Boolean {
|
||||
@@ -161,7 +159,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
|
||||
|
||||
private fun putDataToExtras(intent: Intent?) {
|
||||
intent?.putExtra(AppRouter.KEY_DATA, intent.data)
|
||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||
}
|
||||
|
||||
protected fun setContentViewWebViewSafe(viewBindingProducer: () -> B): Boolean {
|
||||
@@ -180,4 +178,9 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
}
|
||||
|
||||
protected fun hasViewBinding() = ::viewBinding.isInitialized
|
||||
|
||||
companion object {
|
||||
|
||||
const val EXTRA_DATA = "data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import androidx.viewbinding.ViewBinding
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
|
||||
abstract class BaseFragment<B : ViewBinding> :
|
||||
Fragment(),
|
||||
ExceptionResolver.Host {
|
||||
ExceptionResolver.Host,
|
||||
WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
@@ -21,6 +23,9 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
protected lateinit var exceptionResolver: ExceptionResolver
|
||||
private set
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
protected val actionModeDelegate: ActionModeDelegate
|
||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
||||
|
||||
@@ -42,11 +47,15 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
onViewBindingCreated(requireViewBinding(), savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.get
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeInsetsAsPadding
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
@@ -26,6 +32,7 @@ import com.google.android.material.R as materialR
|
||||
@AndroidEntryPoint
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
PreferenceFragmentCompat(),
|
||||
WindowInsetsDelegate.WindowInsetsListener,
|
||||
RecyclerViewOwner,
|
||||
ExceptionResolver.Host {
|
||||
|
||||
@@ -35,7 +42,10 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
override val recyclerView: RecyclerView?
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = listView
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
@@ -49,7 +59,14 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
val themedContext = (view.parentView ?: view).context
|
||||
view.setBackgroundColor(themedContext.getThemeColor(android.R.attr.colorBackground))
|
||||
listView.clipToPadding = false
|
||||
listView.consumeInsetsAsPadding(Gravity.BOTTOM or Gravity.START or Gravity.END)
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -61,10 +78,25 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
listView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun setTitle(title: CharSequence?) {
|
||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||
}
|
||||
|
||||
protected fun startActivitySafe(intent: Intent): Boolean = try {
|
||||
startActivity(intent)
|
||||
true
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
|
||||
private fun focusPreference(key: String) {
|
||||
val pref = findPreference<Preference>(key)
|
||||
if (pref == null) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import androidx.lifecycle.LifecycleService
|
||||
|
||||
abstract class BaseService : LifecycleService()
|
||||
@@ -14,7 +14,6 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -43,8 +42,6 @@ abstract class CoroutineIntentService : BaseService() {
|
||||
intentJobContext.processIntent(intent)
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
intentJobContext.onError(e)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class FragmentContainerActivity(private val fragmentClass: Class<out Fragment>) :
|
||||
BaseActivity<ActivityContainerBinding>(),
|
||||
AppBarOwner,
|
||||
SnackbarOwner {
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = viewBinding.appbar
|
||||
|
||||
override val snackbarHost: CoordinatorLayout
|
||||
get() = viewBinding.root
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityContainerBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val fm = supportFragmentManager
|
||||
if (fm.findFragmentById(R.id.container) == null) {
|
||||
fm.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.container, fragmentClass, getFragmentExtras())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getFragmentExtras(): Bundle? = intent.extras
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -18,7 +17,7 @@ import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
inline fun buildAlertDialog(
|
||||
@UiContext context: Context,
|
||||
context: Context,
|
||||
isCentered: Boolean = false,
|
||||
block: MaterialAlertDialogBuilder.() -> Unit,
|
||||
): AlertDialog = MaterialAlertDialogBuilder(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.koitharu.kotatsu.core.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.net.ConnectivityManagerCompat
|
||||
import dagger.Lazy
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class CommonAlertDialogs @Inject constructor(
|
||||
private val settings: Lazy<AppSettings>,
|
||||
) {
|
||||
|
||||
fun askForDownloadOverMeteredNetwork(
|
||||
@UiContext context: Context,
|
||||
onConfirmed: (allow: Boolean) -> Unit
|
||||
) {
|
||||
when (settings.get().allowDownloadOnMeteredNetwork) {
|
||||
TriStateOption.ENABLED -> onConfirmed(true)
|
||||
TriStateOption.DISABLED -> onConfirmed(false)
|
||||
TriStateOption.ASK -> {
|
||||
if (!ConnectivityManagerCompat.isActiveNetworkMetered(context.connectivityManager)) {
|
||||
onConfirmed(true)
|
||||
return
|
||||
}
|
||||
val listener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
|
||||
onConfirmed(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
BigButtonsAlertDialog.Builder(context)
|
||||
.setIcon(R.drawable.ic_network_cellular)
|
||||
.setTitle(R.string.download_cellular_confirm)
|
||||
.setPositiveButton(R.string.allow_always, listener)
|
||||
.setNeutralButton(R.string.allow_once, listener)
|
||||
.setNegativeButton(R.string.dont_allow, listener)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.ui.dialog
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
@@ -7,15 +10,14 @@ import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.htmlEncode
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.copyToClipboard
|
||||
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.isReportable
|
||||
import org.koitharu.kotatsu.core.util.ext.report
|
||||
import org.koitharu.kotatsu.core.util.ext.requireSerializable
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.DialogErrorDetailsBinding
|
||||
|
||||
class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
@@ -25,7 +27,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val args = requireArguments()
|
||||
exception = args.requireSerializable(AppRouter.KEY_ERROR)
|
||||
exception = args.requireSerializable(ARG_ERROR)
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogErrorDetailsBinding {
|
||||
@@ -39,7 +41,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
text = context.getString(
|
||||
R.string.manga_error_description_pattern,
|
||||
exception.message?.htmlEncode().orEmpty(),
|
||||
arguments?.getString(AppRouter.KEY_URL) ?: exception.getCauseUrl(),
|
||||
arguments?.getString(ARG_URL),
|
||||
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
@@ -51,7 +53,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setNeutralButton(androidx.preference.R.string.copy) { _, _ ->
|
||||
context?.copyToClipboard(getString(R.string.error), exception.stackTraceToString())
|
||||
copyToClipboard()
|
||||
}
|
||||
if (exception.isReportable()) {
|
||||
builder.setPositiveButton(R.string.report) { _, _ ->
|
||||
@@ -61,4 +63,24 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun copyToClipboard() {
|
||||
val clipboardManager = context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
?: return
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(getString(R.string.error), exception.stackTraceToString()),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "ErrorDetailsDialog"
|
||||
private const val ARG_ERROR = "error"
|
||||
private const val ARG_URL = "url"
|
||||
|
||||
fun show(fm: FragmentManager, error: Throwable, url: String?) = ErrorDetailsDialog().withArgs(2) {
|
||||
putSerializable(ARG_ERROR, error)
|
||||
putString(ARG_URL, url)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,11 @@ import android.graphics.Canvas
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import coil3.Image
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
|
||||
import kotlin.math.abs
|
||||
|
||||
class AnimatedFaviconDrawable(
|
||||
@@ -28,12 +23,12 @@ class AnimatedFaviconDrawable(
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
|
||||
private var colorHigh = MaterialColors.harmonize(colorForeground, currentBackgroundColor)
|
||||
private var colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, currentBackgroundColor)
|
||||
private val colorHigh = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||
private val colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, colorBackground)
|
||||
|
||||
init {
|
||||
timeAnimator.setTimeListener(this)
|
||||
onStateChange(state)
|
||||
updateColor()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
@@ -44,11 +39,9 @@ class AnimatedFaviconDrawable(
|
||||
super.draw(canvas)
|
||||
}
|
||||
|
||||
// override fun setAlpha(alpha: Int) = Unit
|
||||
//
|
||||
// override fun getAlpha(): Int = 255
|
||||
//
|
||||
// override fun isOpaque(): Boolean = false
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
|
||||
override fun getAlpha(): Int = 255
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
callback?.also {
|
||||
@@ -67,33 +60,13 @@ class AnimatedFaviconDrawable(
|
||||
|
||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val res = super.onStateChange(state)
|
||||
colorHigh = MaterialColors.harmonize(currentForegroundColor, currentBackgroundColor)
|
||||
colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, currentBackgroundColor)
|
||||
updateColor()
|
||||
return res
|
||||
}
|
||||
|
||||
private fun updateColor() {
|
||||
if (period <= 0f) {
|
||||
return
|
||||
}
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
currentForegroundColor = ArgbEvaluatorCompat.getInstance()
|
||||
colorForeground = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
@StyleRes private val styleResId: Int,
|
||||
) : ((ImageRequest) -> Image?) {
|
||||
|
||||
override fun invoke(request: ImageRequest): Image? {
|
||||
val source = request.getExtra(mangaSourceKey) ?: return null
|
||||
val context = request.context
|
||||
val title = source.getTitle(context)
|
||||
return AnimatedFaviconDrawable(context, styleResId, title).asImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -24,7 +23,6 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
private val interpolator = FastOutSlowInInterpolator()
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
private var currentAlpha: Int = 255
|
||||
|
||||
init {
|
||||
timeAnimator.setTimeListener(this)
|
||||
@@ -40,17 +38,16 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
currentAlpha = alpha
|
||||
updateColor()
|
||||
// this.alpha = alpha FIXME coil's crossfade
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
override fun getAlpha(): Int = currentAlpha
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
override fun getAlpha(): Int = 255
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
callback?.also {
|
||||
@@ -75,10 +72,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
val ph = period / 2
|
||||
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||
currentColor = ColorUtils.setAlphaComponent(
|
||||
ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh),
|
||||
currentAlpha
|
||||
)
|
||||
currentColor = ArgbEvaluatorCompat.getInstance()
|
||||
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.OnLayoutChangeListener
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener
|
||||
import android.widget.ImageView
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Size
|
||||
import coil3.size.ViewSizeResolver
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.roundToInt
|
||||
@@ -19,67 +20,31 @@ class CoverSizeResolver(
|
||||
) : ViewSizeResolver<ImageView> {
|
||||
|
||||
override suspend fun size(): Size {
|
||||
// Fast path: the view is already measured.
|
||||
getSize()?.let { return it }
|
||||
|
||||
// Slow path: wait for the view to be measured.
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val viewTreeObserver = view.viewTreeObserver
|
||||
|
||||
val preDrawListener = object : OnPreDrawListener {
|
||||
private var isResumed = false
|
||||
|
||||
override fun onPreDraw(): Boolean {
|
||||
val size = getSize()
|
||||
if (size != null) {
|
||||
viewTreeObserver.removePreDrawListenerSafe(this)
|
||||
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(size)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
viewTreeObserver.removePreDrawListenerSafe(preDrawListener)
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val layoutListener = LayoutListener(cont)
|
||||
view.addOnLayoutChangeListener(layoutListener)
|
||||
cont.invokeOnCancellation {
|
||||
view.removeOnLayoutChangeListener(layoutListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSize(): Size? {
|
||||
var width = getWidth()
|
||||
var height = getHeight()
|
||||
when {
|
||||
width == null && height == null -> {
|
||||
return null
|
||||
}
|
||||
height == null && width != null -> {
|
||||
height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt())
|
||||
}
|
||||
width == null && height != null -> {
|
||||
width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt())
|
||||
}
|
||||
val lp = view.layoutParams
|
||||
var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
|
||||
var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
|
||||
if (width == null && height == null) {
|
||||
return null
|
||||
}
|
||||
if (height == null && width != null) {
|
||||
height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt())
|
||||
} else if (width == null && height != null) {
|
||||
width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt())
|
||||
}
|
||||
return Size(checkNotNull(width), checkNotNull(height))
|
||||
}
|
||||
|
||||
private fun getWidth() = getDimension(
|
||||
paramSize = view.layoutParams?.width ?: -1,
|
||||
viewSize = view.width,
|
||||
paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0
|
||||
)
|
||||
|
||||
private fun getHeight() = getDimension(
|
||||
paramSize = view.layoutParams?.height ?: -1,
|
||||
viewSize = view.height,
|
||||
paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0
|
||||
)
|
||||
|
||||
private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? {
|
||||
if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
return null
|
||||
@@ -95,11 +60,24 @@ class CoverSizeResolver(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) {
|
||||
if (isAlive) {
|
||||
removeOnPreDrawListener(victim)
|
||||
} else {
|
||||
view.viewTreeObserver.removeOnPreDrawListener(victim)
|
||||
private inner class LayoutListener(
|
||||
private val continuation: CancellableContinuation<Size>,
|
||||
) : OnLayoutChangeListener {
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View,
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int,
|
||||
oldLeft: Int,
|
||||
oldTop: Int,
|
||||
oldRight: Int,
|
||||
oldBottom: Int,
|
||||
) {
|
||||
val size = getSize() ?: return
|
||||
v.removeOnLayoutChangeListener(this)
|
||||
continuation.resume(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,34 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.withClip
|
||||
import coil3.Image
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
import coil3.request.ImageRequest
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getTitle
|
||||
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||
import org.koitharu.kotatsu.core.util.ext.hasFocusStateSpecified
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
|
||||
|
||||
open class FaviconDrawable(
|
||||
context: Context,
|
||||
@StyleRes styleResId: Int,
|
||||
name: String,
|
||||
) : PaintDrawable() {
|
||||
) : Drawable() {
|
||||
|
||||
override val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG)
|
||||
protected var currentBackgroundColor = Color.WHITE
|
||||
private set
|
||||
private var colorBackground: ColorStateList = ColorStateList.valueOf(currentBackgroundColor)
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
protected var colorBackground = Color.WHITE
|
||||
protected var colorForeground = Color.DKGRAY
|
||||
protected var currentForegroundColor = Color.DKGRAY
|
||||
protected var currentStrokeColor = Color.LTGRAY
|
||||
private set
|
||||
private var colorStroke: ColorStateList = ColorStateList.valueOf(currentStrokeColor)
|
||||
private var colorStroke = Color.LTGRAY
|
||||
private val letter = name.take(1).uppercase()
|
||||
private var cornerSize = 0f
|
||||
private var intrinsicSize = -1
|
||||
private val textBounds = Rect()
|
||||
private val tempRect = Rect()
|
||||
private val boundsF = RectF()
|
||||
@@ -49,17 +36,14 @@ open class FaviconDrawable(
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(styleResId, R.styleable.FaviconFallbackDrawable) {
|
||||
colorBackground = getColorStateList(R.styleable.FaviconFallbackDrawable_backgroundColor) ?: colorBackground
|
||||
colorStroke = getColorStateList(R.styleable.FaviconFallbackDrawable_strokeColor) ?: colorStroke
|
||||
colorBackground = getColor(R.styleable.FaviconFallbackDrawable_backgroundColor, colorBackground)
|
||||
colorStroke = getColor(R.styleable.FaviconFallbackDrawable_strokeColor, colorStroke)
|
||||
cornerSize = getDimension(R.styleable.FaviconFallbackDrawable_cornerSize, cornerSize)
|
||||
paint.strokeWidth = getDimension(R.styleable.FaviconFallbackDrawable_strokeWidth, 0f) * 2f
|
||||
intrinsicSize = getDimensionPixelSize(R.styleable.FaviconFallbackDrawable_drawableSize, intrinsicSize)
|
||||
}
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
paint.isFakeBoldText = true
|
||||
colorForeground = KotatsuColors.random(name)
|
||||
currentForegroundColor = MaterialColors.harmonize(colorForeground, colorBackground.defaultColor)
|
||||
onStateChange(state)
|
||||
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
@@ -83,42 +67,31 @@ open class FaviconDrawable(
|
||||
clipPath.close()
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int = intrinsicSize
|
||||
|
||||
override fun getIntrinsicHeight(): Int = intrinsicSize
|
||||
|
||||
override fun isOpaque(): Boolean = cornerSize == 0f && colorBackground.isOpaque
|
||||
|
||||
override fun isStateful(): Boolean = colorStroke.isStateful || colorBackground.isStateful
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun hasFocusStateSpecified(): Boolean =
|
||||
colorBackground.hasFocusStateSpecified() || colorStroke.hasFocusStateSpecified()
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val prevStrokeColor = currentStrokeColor
|
||||
val prevBackgroundColor = currentBackgroundColor
|
||||
currentStrokeColor = colorStroke.getColorForState(state, colorStroke.defaultColor)
|
||||
currentBackgroundColor = colorBackground.getColorForState(state, colorBackground.defaultColor)
|
||||
if (currentBackgroundColor != prevBackgroundColor) {
|
||||
currentForegroundColor = MaterialColors.harmonize(colorForeground, currentBackgroundColor)
|
||||
}
|
||||
return prevBackgroundColor != currentBackgroundColor || prevStrokeColor != currentStrokeColor
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
|
||||
private fun doDraw(canvas: Canvas) {
|
||||
// background
|
||||
paint.color = currentBackgroundColor
|
||||
paint.color = colorBackground
|
||||
paint.style = Paint.Style.FILL
|
||||
canvas.drawPaint(paint)
|
||||
// letter
|
||||
paint.color = currentForegroundColor
|
||||
paint.color = colorForeground
|
||||
val cx = (boundsF.left + boundsF.right) * 0.6f
|
||||
val ty = boundsF.bottom * 0.7f + textBounds.height() * 0.5f - textBounds.bottom
|
||||
canvas.drawText(letter, cx, ty, paint)
|
||||
if (paint.strokeWidth > 0f) {
|
||||
// stroke
|
||||
paint.color = currentStrokeColor
|
||||
paint.color = colorStroke
|
||||
paint.style = Paint.Style.STROKE
|
||||
canvas.drawPath(clipPath, paint)
|
||||
}
|
||||
@@ -130,16 +103,4 @@ open class FaviconDrawable(
|
||||
paint.getTextBounds(text, 0, text.length, tempRect)
|
||||
return testTextSize * width / tempRect.width()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
@StyleRes private val styleResId: Int,
|
||||
) : ((ImageRequest) -> Image?) {
|
||||
|
||||
override fun invoke(request: ImageRequest): Image? {
|
||||
val source = request.getExtra(mangaSourceKey) ?: return null
|
||||
val context = request.context
|
||||
val title = source.getTitle(context)
|
||||
return FaviconDrawable(context, styleResId, title).asImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
abstract class PaintDrawable : Drawable() {
|
||||
|
||||
protected abstract val paint: Paint
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int {
|
||||
return paint.alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? {
|
||||
return paint.colorFilter
|
||||
}
|
||||
|
||||
override fun setDither(dither: Boolean) {
|
||||
paint.isDither = dither
|
||||
}
|
||||
|
||||
override fun setFilterBitmap(filter: Boolean) {
|
||||
paint.isFilterBitmap = filter
|
||||
}
|
||||
|
||||
override fun isFilterBitmap(): Boolean {
|
||||
return paint.isFilterBitmap
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int {
|
||||
if (paint.colorFilter != null) {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
return when (paint.alpha) {
|
||||
0 -> PixelFormat.TRANSPARENT
|
||||
255 -> if (isOpaque()) PixelFormat.OPAQUE else PixelFormat.TRANSLUCENT
|
||||
else -> PixelFormat.TRANSLUCENT
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun isOpaque() = false
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.PaintCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.hasFocusStateSpecified
|
||||
|
||||
class TextDrawable(
|
||||
val text: String,
|
||||
) : PaintDrawable() {
|
||||
|
||||
override val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG)
|
||||
private val textBounds = Rect()
|
||||
private val textPoint = PointF()
|
||||
|
||||
var textSize: Float
|
||||
get() = paint.textSize
|
||||
set(value) {
|
||||
paint.textSize = value
|
||||
measureTextBounds()
|
||||
}
|
||||
|
||||
var textColor: ColorStateList = ColorStateList.valueOf(Color.BLACK)
|
||||
set(value) {
|
||||
field = value
|
||||
onStateChange(state)
|
||||
}
|
||||
|
||||
init {
|
||||
onStateChange(state)
|
||||
measureTextBounds()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawText(text, textPoint.x, textPoint.y, paint)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
textPoint.set(
|
||||
bounds.exactCenterX() - textBounds.exactCenterX(),
|
||||
bounds.exactCenterY() - textBounds.exactCenterY(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int = textBounds.width()
|
||||
|
||||
override fun getIntrinsicHeight(): Int = textBounds.height()
|
||||
|
||||
override fun isStateful(): Boolean = textColor.isStateful
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
override fun hasFocusStateSpecified(): Boolean = textColor.hasFocusStateSpecified()
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
val prevColor = paint.color
|
||||
paint.color = textColor.getColorForState(state, textColor.defaultColor)
|
||||
return paint.color != prevColor
|
||||
}
|
||||
|
||||
private fun measureTextBounds() {
|
||||
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||
onBoundsChange(bounds)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun compound(textView: TextView, text: String): TextDrawable? {
|
||||
val drawable = TextDrawable(text)
|
||||
drawable.textSize = textView.textSize
|
||||
drawable.textColor = textView.textColors
|
||||
return drawable.takeIf {
|
||||
PaintCompat.hasGlyph(drawable.paint, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.Gravity
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.GravityInt
|
||||
import coil3.target.GenericViewTarget
|
||||
|
||||
class TextViewTarget(
|
||||
override val view: TextView,
|
||||
@GravityInt compoundDrawable: Int,
|
||||
) : GenericViewTarget<TextView>() {
|
||||
|
||||
private val drawableIndex: Int = when (compoundDrawable) {
|
||||
Gravity.START -> 0
|
||||
Gravity.TOP -> 2
|
||||
Gravity.END -> 3
|
||||
Gravity.BOTTOM -> 4
|
||||
else -> -1
|
||||
}
|
||||
|
||||
override var drawable: Drawable?
|
||||
get() = if (drawableIndex != -1) {
|
||||
view.compoundDrawablesRelative[drawableIndex]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
set(value) {
|
||||
if (drawableIndex == -1) {
|
||||
return
|
||||
}
|
||||
val drawables = view.compoundDrawablesRelative
|
||||
drawables[drawableIndex] = value
|
||||
view.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
drawables[0],
|
||||
drawables[1],
|
||||
drawables[2],
|
||||
drawables[3],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,8 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
init {
|
||||
fastScroller.id = R.id.fast_scroller
|
||||
fastScroller.layoutParams = ViewGroup.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.*
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DimenRes
|
||||
@@ -164,7 +162,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
|
||||
super.onSizeChanged(w, h, oldW, oldH)
|
||||
viewHeight = h - paddingTop - paddingBottom
|
||||
viewHeight = h
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -231,7 +229,6 @@ class FastScroller @JvmOverloads constructor(
|
||||
*
|
||||
* @param params The [ViewGroup.LayoutParams] for this view, cannot be null
|
||||
*/
|
||||
@Suppress("RemoveRedundantQualifierName")
|
||||
override fun setLayoutParams(params: ViewGroup.LayoutParams) {
|
||||
params.width = LayoutParams.WRAP_CONTENT
|
||||
super.setLayoutParams(params)
|
||||
@@ -532,7 +529,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
private fun findValidParent(view: View): ViewGroup? = view.ancestors.firstNotNullOfOrNull { p ->
|
||||
if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) {
|
||||
p
|
||||
p as ViewGroup
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user